feat: initial oauth implementation
960999bb
10 file(s) · +812 −44
| 24 | 24 | }, |
|
| 25 | 25 | "packages/cli": { |
|
| 26 | 26 | "name": "sequoia-cli", |
|
| 27 | - | "version": "0.2.0", |
|
| 27 | + | "version": "0.2.1", |
|
| 28 | 28 | "bin": { |
|
| 29 | 29 | "sequoia": "dist/index.js", |
|
| 30 | 30 | }, |
|
| 31 | 31 | "dependencies": { |
|
| 32 | 32 | "@atproto/api": "^0.18.17", |
|
| 33 | + | "@atproto/oauth-client-node": "^0.3.16", |
|
| 33 | 34 | "@clack/prompts": "^1.0.0", |
|
| 34 | 35 | "cmd-ts": "^0.14.3", |
|
| 35 | 36 | "glob": "^13.0.0", |
|
| 36 | 37 | "mime-types": "^2.1.35", |
|
| 37 | 38 | "minimatch": "^10.1.1", |
|
| 39 | + | "open": "^11.0.0", |
|
| 38 | 40 | }, |
|
| 39 | 41 | "devDependencies": { |
|
| 40 | 42 | "@biomejs/biome": "^2.3.13", |
|
| 49 | 51 | "packages": { |
|
| 50 | 52 | "@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="], |
|
| 51 | 53 | ||
| 54 | + | "@atproto-labs/did-resolver": ["@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.23.8" } }, "sha512-2K1bC04nI2fmgNcvof+yA28IhGlpWn2JKYlPa7To9JTKI45FINCGkQSGiL2nyXlyzDJJ34fZ1aq6/IRFIOIiqg=="], |
|
| 55 | + | ||
| 56 | + | "@atproto-labs/fetch": ["@atproto-labs/fetch@0.2.3", "", { "dependencies": { "@atproto-labs/pipe": "0.1.1" } }, "sha512-NZtbJOCbxKUFRFKMpamT38PUQMY0hX0p7TG5AEYOPhZKZEP7dHZ1K2s1aB8MdVH0qxmqX7nQleNrrvLf09Zfdw=="], |
|
| 57 | + | ||
| 58 | + | "@atproto-labs/fetch-node": ["@atproto-labs/fetch-node@0.2.0", "", { "dependencies": { "@atproto-labs/fetch": "0.2.3", "@atproto-labs/pipe": "0.1.1", "ipaddr.js": "^2.1.0", "undici": "^6.14.1" } }, "sha512-Krq09nH/aeoiU2s9xdHA0FjTEFWG9B5FFenipv1iRixCcPc7V3DhTNDawxG9gI8Ny0k4dBVS9WTRN/IDzBx86Q=="], |
|
| 59 | + | ||
| 60 | + | "@atproto-labs/handle-resolver": ["@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.23.8" } }, "sha512-qnSTXvOBNj1EHhp2qTWSX8MS5q3AwYU5LKlt5fBvSbCjgmTr2j0URHCv+ydrwO55KvsojIkTMgeMOh4YuY4fCA=="], |
|
| 61 | + | ||
| 62 | + | "@atproto-labs/handle-resolver-node": ["@atproto-labs/handle-resolver-node@0.1.25", "", { "dependencies": { "@atproto-labs/fetch-node": "0.2.0", "@atproto-labs/handle-resolver": "0.3.6", "@atproto/did": "0.3.0" } }, "sha512-NY9WYM2VLd3IuMGRkkmvGBg8xqVEaK/fitv1vD8SMXqFTekdpjOLCCyv7EFtqVHouzmDcL83VOvWRfHVa8V9Yw=="], |
|
| 63 | + | ||
| 64 | + | "@atproto-labs/identity-resolver": ["@atproto-labs/identity-resolver@0.3.6", "", { "dependencies": { "@atproto-labs/did-resolver": "0.2.6", "@atproto-labs/handle-resolver": "0.3.6" } }, "sha512-qoWqBDRobln0NR8L8dQjSp79E0chGkBhibEgxQa2f9WD+JbJdjQ0YvwwO5yeQn05pJoJmAwmI2wyJ45zjU7aWg=="], |
|
| 65 | + | ||
| 66 | + | "@atproto-labs/pipe": ["@atproto-labs/pipe@0.1.1", "", {}, "sha512-hdNw2oUs2B6BN1lp+32pF7cp8EMKuIN5Qok2Vvv/aOpG/3tNSJ9YkvfI0k6Zd188LeDDYRUpYpxcoFIcGH/FNg=="], |
|
| 67 | + | ||
| 68 | + | "@atproto-labs/simple-store": ["@atproto-labs/simple-store@0.3.0", "", {}, "sha512-nOb6ONKBRJHRlukW1sVawUkBqReLlLx6hT35VS3imaNPwiXDxLnTK7lxw3Lrl9k5yugSBDQAkZAq3MPTEFSUBQ=="], |
|
| 69 | + | ||
| 70 | + | "@atproto-labs/simple-store-memory": ["@atproto-labs/simple-store-memory@0.1.4", "", { "dependencies": { "@atproto-labs/simple-store": "0.3.0", "lru-cache": "^10.2.0" } }, "sha512-3mKY4dP8I7yKPFj9VKpYyCRzGJOi5CEpOLPlRhoJyLmgs3J4RzDrjn323Oakjz2Aj2JzRU/AIvWRAZVhpYNJHw=="], |
|
| 71 | + | ||
| 52 | 72 | "@atproto/api": ["@atproto/api@0.18.17", "", { "dependencies": { "@atproto/common-web": "^0.4.13", "@atproto/lexicon": "^0.6.1", "@atproto/syntax": "^0.4.3", "@atproto/xrpc": "^0.7.7", "await-lock": "^2.2.2", "multiformats": "^9.9.0", "tlds": "^1.234.0", "zod": "^3.23.8" } }, "sha512-TeJkLGPkiK3jblwTDSNTH+CnS6WgaOiHDZeVVzywtxomyyF0FpQVSMz5eP3sDhxyHJqpI3E2AOYD7PO/JSbzJw=="], |
|
| 53 | 73 | ||
| 54 | 74 | "@atproto/common-web": ["@atproto/common-web@0.4.13", "", { "dependencies": { "@atproto/lex-data": "0.0.9", "@atproto/lex-json": "0.0.9", "@atproto/syntax": "0.4.3", "zod": "^3.23.8" } }, "sha512-TewRUyB/dVJ5PtI3QmJzEgT3wDsvpnLJ+48hPl+LuUueJPamZevXKJN6dFjtbKAMFRnl2bKfdsf79qwvdSaLKQ=="], |
|
| 55 | 75 | ||
| 76 | + | "@atproto/did": ["@atproto/did@0.3.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-raUPzUGegtW/6OxwCmM8bhZvuIMzxG5t9oWsth6Tp91Kb5fTnHV2h/KKNF1C82doeA4BdXCErTyg7ISwLbQkzA=="], |
|
| 77 | + | ||
| 78 | + | "@atproto/jwk": ["@atproto/jwk@0.6.0", "", { "dependencies": { "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-bDoJPvt7TrQVi/rBfBrSSpGykhtIriKxeYCYQTiPRKFfyRhbgpElF0wPXADjIswnbzZdOwbY63az4E/CFVT3Tw=="], |
|
| 79 | + | ||
| 80 | + | "@atproto/jwk-jose": ["@atproto/jwk-jose@0.1.11", "", { "dependencies": { "@atproto/jwk": "0.6.0", "jose": "^5.2.0" } }, "sha512-i4Fnr2sTBYmMmHXl7NJh8GrCH+tDQEVWrcDMDnV5DjJfkgT17wIqvojIw9SNbSL4Uf0OtfEv6AgG0A+mgh8b5Q=="], |
|
| 81 | + | ||
| 82 | + | "@atproto/jwk-webcrypto": ["@atproto/jwk-webcrypto@0.2.0", "", { "dependencies": { "@atproto/jwk": "0.6.0", "@atproto/jwk-jose": "0.1.11", "zod": "^3.23.8" } }, "sha512-UmgRrrEAkWvxwhlwe30UmDOdTEFidlIzBC7C3cCbeJMcBN1x8B3KH+crXrsTqfWQBG58mXgt8wgSK3Kxs2LhFg=="], |
|
| 83 | + | ||
| 56 | 84 | "@atproto/lex-data": ["@atproto/lex-data@0.0.9", "", { "dependencies": { "multiformats": "^9.9.0", "tslib": "^2.8.1", "uint8arrays": "3.0.0", "unicode-segmenter": "^0.14.0" } }, "sha512-1slwe4sG0cyWtsq16+rBoWIxNDqGPkkvN+PV6JuzA7dgUK9bjUmXBGQU4eZlUPSS43X1Nhmr/9VjgKmEzU9vDw=="], |
|
| 57 | 85 | ||
| 58 | 86 | "@atproto/lex-json": ["@atproto/lex-json@0.0.9", "", { "dependencies": { "@atproto/lex-data": "0.0.9", "tslib": "^2.8.1" } }, "sha512-Q2v1EVZcnd+ndyZj1r2UlGikA7q6It24CFPLbxokcf5Ba4RBupH8IkkQX7mqUDSRWPgQdmZYIdW9wUln+MKDqw=="], |
|
| 59 | 87 | ||
| 60 | 88 | "@atproto/lexicon": ["@atproto/lexicon@0.6.1", "", { "dependencies": { "@atproto/common-web": "^0.4.13", "@atproto/syntax": "^0.4.3", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-/vI1kVlY50Si+5MXpvOucelnYwb0UJ6Qto5mCp+7Q5C+Jtp+SoSykAPVvjVtTnQUH2vrKOFOwpb3C375vSKzXw=="], |
|
| 61 | 89 | ||
| 90 | + | "@atproto/oauth-client": ["@atproto/oauth-client@0.5.14", "", { "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.2", "@atproto/xrpc": "0.7.7", "core-js": "^3", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-sPH+vcdq9maTEAhJI0HzmFcFAMrkCS19np+RUssNkX6kS8Xr3OYr57tvYRCbkcnIyYTfYcxKQgpwHKx3RVEaYw=="], |
|
| 91 | + | ||
| 92 | + | "@atproto/oauth-client-node": ["@atproto/oauth-client-node@0.3.16", "", { "dependencies": { "@atproto-labs/did-resolver": "0.2.6", "@atproto-labs/handle-resolver-node": "0.1.25", "@atproto-labs/simple-store": "0.3.0", "@atproto/did": "0.3.0", "@atproto/jwk": "0.6.0", "@atproto/jwk-jose": "0.1.11", "@atproto/jwk-webcrypto": "0.2.0", "@atproto/oauth-client": "0.5.14", "@atproto/oauth-types": "0.6.2" } }, "sha512-2dooMzxAkiQ4MkOAZlEQ3iwbB9SEovrbIKMNuBbVCLQYORVNxe20tMdjs3lvhrzdpzvaHLlQnJJhw5dA9VELFw=="], |
|
| 93 | + | ||
| 94 | + | "@atproto/oauth-types": ["@atproto/oauth-types@0.6.2", "", { "dependencies": { "@atproto/did": "0.3.0", "@atproto/jwk": "0.6.0", "zod": "^3.23.8" } }, "sha512-2cuboM4RQBCYR8NQC5uGRkW6KgCgKyq/B5/+tnMmWZYtZGVUQvsUWQHK/ZiMCnVXbcDNtc/RIEJQJDZ8FXMoxg=="], |
|
| 95 | + | ||
| 62 | 96 | "@atproto/syntax": ["@atproto/syntax@0.4.3", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-YoZUz40YAJr5nPwvCDWgodEOlt5IftZqPJvA0JDWjuZKD8yXddTwSzXSaKQAzGOpuM+/A3uXRtPzJJqlScc+iA=="], |
|
| 63 | 97 | ||
| 64 | 98 | "@atproto/xrpc": ["@atproto/xrpc@0.7.7", "", { "dependencies": { "@atproto/lexicon": "^0.6.0", "zod": "^3.23.8" } }, "sha512-K1ZyO/BU8JNtXX5dmPp7b5UrkLMMqpsIa/Lrj5D3Su+j1Xwq1m6QJ2XJ1AgjEjkI1v4Muzm7klianLE6XGxtmA=="], |
|
| 615 | 649 | ||
| 616 | 650 | "bun-types": ["bun-types@1.3.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-qyschsA03Qz+gou+apt6HNl6HnI+sJJLL4wLDke4iugsE6584CMupOtTY1n+2YC9nGVrEKUlTs99jjRLKgWnjQ=="], |
|
| 617 | 651 | ||
| 652 | + | "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], |
|
| 653 | + | ||
| 618 | 654 | "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], |
|
| 619 | 655 | ||
| 620 | 656 | "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], |
|
| 662 | 698 | "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], |
|
| 663 | 699 | ||
| 664 | 700 | "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], |
|
| 701 | + | ||
| 702 | + | "core-js": ["core-js@3.48.0", "", {}, "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ=="], |
|
| 665 | 703 | ||
| 666 | 704 | "cose-base": ["cose-base@1.0.3", "", { "dependencies": { "layout-base": "^1.0.0" } }, "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg=="], |
|
| 667 | 705 | ||
| 761 | 799 | ||
| 762 | 800 | "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], |
|
| 763 | 801 | ||
| 802 | + | "default-browser": ["default-browser@5.4.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg=="], |
|
| 803 | + | ||
| 804 | + | "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], |
|
| 805 | + | ||
| 806 | + | "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="], |
|
| 807 | + | ||
| 764 | 808 | "delaunator": ["delaunator@5.0.1", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw=="], |
|
| 765 | 809 | ||
| 766 | 810 | "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], |
|
| 921 | 965 | ||
| 922 | 966 | "internmap": ["internmap@1.0.1", "", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="], |
|
| 923 | 967 | ||
| 968 | + | "ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="], |
|
| 969 | + | ||
| 924 | 970 | "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], |
|
| 925 | 971 | ||
| 926 | 972 | "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="], |
|
| 927 | 973 | ||
| 928 | 974 | "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="], |
|
| 929 | 975 | ||
| 976 | + | "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], |
|
| 977 | + | ||
| 930 | 978 | "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], |
|
| 931 | 979 | ||
| 980 | + | "is-in-ssh": ["is-in-ssh@1.0.0", "", {}, "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw=="], |
|
| 981 | + | ||
| 982 | + | "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], |
|
| 983 | + | ||
| 932 | 984 | "is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="], |
|
| 933 | 985 | ||
| 934 | 986 | "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], |
|
| 937 | 989 | ||
| 938 | 990 | "is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="], |
|
| 939 | 991 | ||
| 992 | + | "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], |
|
| 993 | + | ||
| 940 | 994 | "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], |
|
| 941 | 995 | ||
| 942 | 996 | "iso-datestring-validator": ["iso-datestring-validator@2.2.2", "", {}, "sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA=="], |
|
| 944 | 998 | "javascript-stringify": ["javascript-stringify@2.1.0", "", {}, "sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg=="], |
|
| 945 | 999 | ||
| 946 | 1000 | "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], |
|
| 1001 | + | ||
| 1002 | + | "jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="], |
|
| 947 | 1003 | ||
| 948 | 1004 | "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], |
|
| 949 | 1005 | ||
| 1166 | 1222 | "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], |
|
| 1167 | 1223 | ||
| 1168 | 1224 | "oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="], |
|
| 1225 | + | ||
| 1226 | + | "open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="], |
|
| 1169 | 1227 | ||
| 1170 | 1228 | "ora": ["ora@7.0.1", "", { "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^4.0.0", "cli-spinners": "^2.9.0", "is-interactive": "^2.0.0", "is-unicode-supported": "^1.3.0", "log-symbols": "^5.1.0", "stdin-discarder": "^0.1.0", "string-width": "^6.1.0", "strip-ansi": "^7.1.0" } }, "sha512-0TUxTiFJWv+JnjWm4o9yvuskpEJLXTcng8MJuKd+SzAzp2o+OP3HWqNhB4OdJRt1Vsd9/mR0oyaEYlOnL7XIRw=="], |
|
| 1171 | 1229 | ||
| 1209 | 1267 | ||
| 1210 | 1268 | "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], |
|
| 1211 | 1269 | ||
| 1270 | + | "powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="], |
|
| 1271 | + | ||
| 1212 | 1272 | "property-information": ["property-information@6.5.0", "", {}, "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig=="], |
|
| 1213 | 1273 | ||
| 1214 | 1274 | "radix-ui": ["radix-ui@1.4.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-accessible-icon": "1.1.7", "@radix-ui/react-accordion": "1.2.12", "@radix-ui/react-alert-dialog": "1.1.15", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-aspect-ratio": "1.1.7", "@radix-ui/react-avatar": "1.1.10", "@radix-ui/react-checkbox": "1.3.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-context-menu": "2.2.16", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-dropdown-menu": "2.1.16", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-form": "0.1.8", "@radix-ui/react-hover-card": "1.1.15", "@radix-ui/react-label": "2.1.7", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-menubar": "1.1.16", "@radix-ui/react-navigation-menu": "1.2.14", "@radix-ui/react-one-time-password-field": "0.1.8", "@radix-ui/react-password-toggle-field": "0.1.3", "@radix-ui/react-popover": "1.1.15", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-progress": "1.1.7", "@radix-ui/react-radio-group": "1.3.8", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-scroll-area": "1.2.10", "@radix-ui/react-select": "2.2.6", "@radix-ui/react-separator": "1.1.7", "@radix-ui/react-slider": "1.3.6", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-switch": "1.2.6", "@radix-ui/react-tabs": "1.1.13", "@radix-ui/react-toast": "1.2.15", "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-toggle-group": "1.1.11", "@radix-ui/react-toolbar": "1.1.11", "@radix-ui/react-tooltip": "1.2.8", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-escape-keydown": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA=="], |
|
| 1282 | 1342 | "rollup": ["rollup@4.57.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.0", "@rollup/rollup-android-arm64": "4.57.0", "@rollup/rollup-darwin-arm64": "4.57.0", "@rollup/rollup-darwin-x64": "4.57.0", "@rollup/rollup-freebsd-arm64": "4.57.0", "@rollup/rollup-freebsd-x64": "4.57.0", "@rollup/rollup-linux-arm-gnueabihf": "4.57.0", "@rollup/rollup-linux-arm-musleabihf": "4.57.0", "@rollup/rollup-linux-arm64-gnu": "4.57.0", "@rollup/rollup-linux-arm64-musl": "4.57.0", "@rollup/rollup-linux-loong64-gnu": "4.57.0", "@rollup/rollup-linux-loong64-musl": "4.57.0", "@rollup/rollup-linux-ppc64-gnu": "4.57.0", "@rollup/rollup-linux-ppc64-musl": "4.57.0", "@rollup/rollup-linux-riscv64-gnu": "4.57.0", "@rollup/rollup-linux-riscv64-musl": "4.57.0", "@rollup/rollup-linux-s390x-gnu": "4.57.0", "@rollup/rollup-linux-x64-gnu": "4.57.0", "@rollup/rollup-linux-x64-musl": "4.57.0", "@rollup/rollup-openbsd-x64": "4.57.0", "@rollup/rollup-openharmony-arm64": "4.57.0", "@rollup/rollup-win32-arm64-msvc": "4.57.0", "@rollup/rollup-win32-ia32-msvc": "4.57.0", "@rollup/rollup-win32-x64-gnu": "4.57.0", "@rollup/rollup-win32-x64-msvc": "4.57.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-e5lPJi/aui4TO1LpAXIRLySmwXSE8k3b9zoGfd42p67wzxog4WHjiZF3M2uheQih4DGyc25QEV4yRBbpueNiUA=="], |
|
| 1283 | 1343 | ||
| 1284 | 1344 | "roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="], |
|
| 1345 | + | ||
| 1346 | + | "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], |
|
| 1285 | 1347 | ||
| 1286 | 1348 | "rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="], |
|
| 1287 | 1349 | ||
| 1375 | 1437 | ||
| 1376 | 1438 | "uint8arrays": ["uint8arrays@3.0.0", "", { "dependencies": { "multiformats": "^9.4.2" } }, "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA=="], |
|
| 1377 | 1439 | ||
| 1440 | + | "undici": ["undici@6.23.0", "", {}, "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g=="], |
|
| 1441 | + | ||
| 1378 | 1442 | "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], |
|
| 1379 | 1443 | ||
| 1380 | 1444 | "unicode-segmenter": ["unicode-segmenter@0.14.5", "", {}, "sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g=="], |
|
| 1444 | 1508 | "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], |
|
| 1445 | 1509 | ||
| 1446 | 1510 | "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], |
|
| 1511 | + | ||
| 1512 | + | "wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="], |
|
| 1447 | 1513 | ||
| 1448 | 1514 | "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], |
|
| 1449 | 1515 | ||
| 30 | 30 | }, |
|
| 31 | 31 | "dependencies": { |
|
| 32 | 32 | "@atproto/api": "^0.18.17", |
|
| 33 | + | "@atproto/oauth-client-node": "^0.3.16", |
|
| 33 | 34 | "@clack/prompts": "^1.0.0", |
|
| 34 | 35 | "cmd-ts": "^0.14.3", |
|
| 35 | 36 | "glob": "^13.0.0", |
|
| 36 | 37 | "mime-types": "^2.1.35", |
|
| 37 | - | "minimatch": "^10.1.1" |
|
| 38 | + | "minimatch": "^10.1.1", |
|
| 39 | + | "open": "^11.0.0" |
|
| 38 | 40 | } |
|
| 39 | 41 | } |
| 158 | 158 | ||
| 159 | 159 | // Save credentials |
|
| 160 | 160 | await saveCredentials({ |
|
| 161 | + | type: "app-password", |
|
| 161 | 162 | pdsUrl, |
|
| 162 | 163 | identifier: identifier, |
|
| 163 | 164 | password: appPassword, |
| 1 | + | import * as http from "node:http"; |
|
| 2 | + | import { log, note, select, spinner, text } from "@clack/prompts"; |
|
| 3 | + | import { command, flag, option, optional, string } from "cmd-ts"; |
|
| 4 | + | import { resolveHandleToDid } from "../lib/atproto"; |
|
| 5 | + | import { |
|
| 6 | + | getCallbackPort, |
|
| 7 | + | getCallbackUrl, |
|
| 8 | + | getOAuthClient, |
|
| 9 | + | getOAuthScope, |
|
| 10 | + | } from "../lib/oauth-client"; |
|
| 11 | + | import { |
|
| 12 | + | deleteOAuthSession, |
|
| 13 | + | getOAuthStorePath, |
|
| 14 | + | listOAuthSessions, |
|
| 15 | + | } from "../lib/oauth-store"; |
|
| 16 | + | import { exitOnCancel } from "../lib/prompts"; |
|
| 17 | + | ||
| 18 | + | const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes |
|
| 19 | + | ||
| 20 | + | export const loginCommand = command({ |
|
| 21 | + | name: "login", |
|
| 22 | + | description: "Login with OAuth (browser-based authentication)", |
|
| 23 | + | args: { |
|
| 24 | + | logout: option({ |
|
| 25 | + | long: "logout", |
|
| 26 | + | description: "Remove OAuth session for a specific DID", |
|
| 27 | + | type: optional(string), |
|
| 28 | + | }), |
|
| 29 | + | list: flag({ |
|
| 30 | + | long: "list", |
|
| 31 | + | description: "List all stored OAuth sessions", |
|
| 32 | + | }), |
|
| 33 | + | }, |
|
| 34 | + | handler: async ({ logout, list }) => { |
|
| 35 | + | // List sessions |
|
| 36 | + | if (list) { |
|
| 37 | + | const sessions = await listOAuthSessions(); |
|
| 38 | + | if (sessions.length === 0) { |
|
| 39 | + | log.info("No OAuth sessions stored"); |
|
| 40 | + | } else { |
|
| 41 | + | log.info("OAuth sessions:"); |
|
| 42 | + | for (const did of sessions) { |
|
| 43 | + | console.log(` - ${did}`); |
|
| 44 | + | } |
|
| 45 | + | } |
|
| 46 | + | return; |
|
| 47 | + | } |
|
| 48 | + | ||
| 49 | + | // Logout |
|
| 50 | + | if (logout !== undefined) { |
|
| 51 | + | const did = logout || undefined; |
|
| 52 | + | ||
| 53 | + | if (!did) { |
|
| 54 | + | // No DID provided - show available and prompt |
|
| 55 | + | const sessions = await listOAuthSessions(); |
|
| 56 | + | if (sessions.length === 0) { |
|
| 57 | + | log.info("No OAuth sessions found"); |
|
| 58 | + | return; |
|
| 59 | + | } |
|
| 60 | + | if (sessions.length === 1) { |
|
| 61 | + | const deleted = await deleteOAuthSession(sessions[0]!); |
|
| 62 | + | if (deleted) { |
|
| 63 | + | log.success(`Removed OAuth session for ${sessions[0]}`); |
|
| 64 | + | } |
|
| 65 | + | return; |
|
| 66 | + | } |
|
| 67 | + | // Multiple sessions - prompt |
|
| 68 | + | const selected = exitOnCancel( |
|
| 69 | + | await select({ |
|
| 70 | + | message: "Select session to remove:", |
|
| 71 | + | options: sessions.map((d) => ({ value: d, label: d })), |
|
| 72 | + | }), |
|
| 73 | + | ); |
|
| 74 | + | const deleted = await deleteOAuthSession(selected); |
|
| 75 | + | if (deleted) { |
|
| 76 | + | log.success(`Removed OAuth session for ${selected}`); |
|
| 77 | + | } |
|
| 78 | + | return; |
|
| 79 | + | } |
|
| 80 | + | ||
| 81 | + | const deleted = await deleteOAuthSession(did); |
|
| 82 | + | if (deleted) { |
|
| 83 | + | log.success(`Removed OAuth session for ${did}`); |
|
| 84 | + | } else { |
|
| 85 | + | log.info(`No OAuth session found for ${did}`); |
|
| 86 | + | } |
|
| 87 | + | return; |
|
| 88 | + | } |
|
| 89 | + | ||
| 90 | + | // OAuth login flow |
|
| 91 | + | note( |
|
| 92 | + | "OAuth login will open your browser to authenticate.\n\n" + |
|
| 93 | + | "This is more secure than app passwords and tokens refresh automatically.", |
|
| 94 | + | "OAuth Login", |
|
| 95 | + | ); |
|
| 96 | + | ||
| 97 | + | const handle = exitOnCancel( |
|
| 98 | + | await text({ |
|
| 99 | + | message: "Handle or DID:", |
|
| 100 | + | placeholder: "yourhandle.bsky.social", |
|
| 101 | + | }), |
|
| 102 | + | ); |
|
| 103 | + | ||
| 104 | + | if (!handle) { |
|
| 105 | + | log.error("Handle is required"); |
|
| 106 | + | process.exit(1); |
|
| 107 | + | } |
|
| 108 | + | ||
| 109 | + | const s = spinner(); |
|
| 110 | + | s.start("Resolving identity..."); |
|
| 111 | + | ||
| 112 | + | let did: string; |
|
| 113 | + | try { |
|
| 114 | + | did = await resolveHandleToDid(handle); |
|
| 115 | + | s.stop(`Identity resolved`); |
|
| 116 | + | } catch (error) { |
|
| 117 | + | s.stop("Failed to resolve identity"); |
|
| 118 | + | if (error instanceof Error) { |
|
| 119 | + | log.error(`Error: ${error.message}`); |
|
| 120 | + | } else { |
|
| 121 | + | log.error(`Error: ${error}`); |
|
| 122 | + | } |
|
| 123 | + | process.exit(1); |
|
| 124 | + | } |
|
| 125 | + | ||
| 126 | + | s.start("Initializing OAuth..."); |
|
| 127 | + | ||
| 128 | + | try { |
|
| 129 | + | const client = await getOAuthClient(); |
|
| 130 | + | ||
| 131 | + | // Generate authorization URL using the resolved DID |
|
| 132 | + | const authUrl = await client.authorize(did, { |
|
| 133 | + | scope: getOAuthScope(), |
|
| 134 | + | }); |
|
| 135 | + | ||
| 136 | + | log.info(`Login URL: ${authUrl}`); |
|
| 137 | + | ||
| 138 | + | s.message("Opening browser..."); |
|
| 139 | + | ||
| 140 | + | // Try to open browser |
|
| 141 | + | let browserOpened = true; |
|
| 142 | + | try { |
|
| 143 | + | const open = (await import("open")).default; |
|
| 144 | + | await open(authUrl.toString()); |
|
| 145 | + | } catch { |
|
| 146 | + | browserOpened = false; |
|
| 147 | + | } |
|
| 148 | + | ||
| 149 | + | s.message("Waiting for authentication..."); |
|
| 150 | + | ||
| 151 | + | // Show URL info |
|
| 152 | + | if (!browserOpened) { |
|
| 153 | + | s.stop("Could not open browser automatically"); |
|
| 154 | + | log.warn("Please open the following URL in your browser:"); |
|
| 155 | + | log.info(authUrl.toString()); |
|
| 156 | + | s.start("Waiting for authentication..."); |
|
| 157 | + | } |
|
| 158 | + | ||
| 159 | + | // Start HTTP server to receive callback |
|
| 160 | + | const result = await waitForCallback(); |
|
| 161 | + | ||
| 162 | + | if (!result.success) { |
|
| 163 | + | s.stop("Authentication failed"); |
|
| 164 | + | log.error(result.error || "OAuth callback failed"); |
|
| 165 | + | process.exit(1); |
|
| 166 | + | } |
|
| 167 | + | ||
| 168 | + | s.message("Completing authentication..."); |
|
| 169 | + | ||
| 170 | + | // Exchange code for tokens |
|
| 171 | + | const { session } = await client.callback( |
|
| 172 | + | new URLSearchParams(result.params!), |
|
| 173 | + | ); |
|
| 174 | + | ||
| 175 | + | // Try to get the handle for display (use the original handle input as fallback) |
|
| 176 | + | let displayName = handle; |
|
| 177 | + | try { |
|
| 178 | + | // The session should have the DID, we can use the original handle they entered |
|
| 179 | + | // or we could fetch the profile to get the current handle |
|
| 180 | + | displayName = handle.startsWith("did:") ? session.did : handle; |
|
| 181 | + | } catch { |
|
| 182 | + | displayName = session.did; |
|
| 183 | + | } |
|
| 184 | + | ||
| 185 | + | s.stop(`Logged in as ${displayName}`); |
|
| 186 | + | ||
| 187 | + | log.success(`OAuth session saved to ${getOAuthStorePath()}`); |
|
| 188 | + | log.info("Your session will refresh automatically when needed."); |
|
| 189 | + | ||
| 190 | + | // Exit cleanly - the OAuth client may have background processes |
|
| 191 | + | process.exit(0); |
|
| 192 | + | } catch (error) { |
|
| 193 | + | s.stop("OAuth login failed"); |
|
| 194 | + | if (error instanceof Error) { |
|
| 195 | + | log.error(`Error: ${error.message}`); |
|
| 196 | + | } else { |
|
| 197 | + | log.error(`Error: ${error}`); |
|
| 198 | + | } |
|
| 199 | + | process.exit(1); |
|
| 200 | + | } |
|
| 201 | + | }, |
|
| 202 | + | }); |
|
| 203 | + | ||
| 204 | + | interface CallbackResult { |
|
| 205 | + | success: boolean; |
|
| 206 | + | params?: Record<string, string>; |
|
| 207 | + | error?: string; |
|
| 208 | + | } |
|
| 209 | + | ||
| 210 | + | function waitForCallback(): Promise<CallbackResult> { |
|
| 211 | + | return new Promise((resolve) => { |
|
| 212 | + | const port = getCallbackPort(); |
|
| 213 | + | let timeoutId: ReturnType<typeof setTimeout> | undefined; |
|
| 214 | + | ||
| 215 | + | const server = http.createServer((req, res) => { |
|
| 216 | + | const url = new URL(req.url || "/", `http://127.0.0.1:${port}`); |
|
| 217 | + | ||
| 218 | + | if (url.pathname === "/oauth/callback") { |
|
| 219 | + | const params: Record<string, string> = {}; |
|
| 220 | + | url.searchParams.forEach((value, key) => { |
|
| 221 | + | params[key] = value; |
|
| 222 | + | }); |
|
| 223 | + | ||
| 224 | + | // Clear the timeout |
|
| 225 | + | if (timeoutId) clearTimeout(timeoutId); |
|
| 226 | + | ||
| 227 | + | // Check for error |
|
| 228 | + | if (params.error) { |
|
| 229 | + | res.writeHead(200, { "Content-Type": "text/html" }); |
|
| 230 | + | res.end(` |
|
| 231 | + | <html> |
|
| 232 | + | <body style="font-family: system-ui; padding: 2rem; text-align: center;"> |
|
| 233 | + | <h1>Authentication Failed</h1> |
|
| 234 | + | <p>${params.error_description || params.error}</p> |
|
| 235 | + | <p>You can close this window.</p> |
|
| 236 | + | </body> |
|
| 237 | + | </html> |
|
| 238 | + | `); |
|
| 239 | + | server.close(() => { |
|
| 240 | + | resolve({ |
|
| 241 | + | success: false, |
|
| 242 | + | error: params.error_description || params.error, |
|
| 243 | + | }); |
|
| 244 | + | }); |
|
| 245 | + | return; |
|
| 246 | + | } |
|
| 247 | + | ||
| 248 | + | // Success |
|
| 249 | + | res.writeHead(200, { "Content-Type": "text/html" }); |
|
| 250 | + | res.end(` |
|
| 251 | + | <html> |
|
| 252 | + | <body style="font-family: system-ui; padding: 2rem; text-align: center;"> |
|
| 253 | + | <h1>Authentication Successful</h1> |
|
| 254 | + | <p>You can close this window and return to the terminal.</p> |
|
| 255 | + | </body> |
|
| 256 | + | </html> |
|
| 257 | + | `); |
|
| 258 | + | server.close(() => { |
|
| 259 | + | resolve({ success: true, params }); |
|
| 260 | + | }); |
|
| 261 | + | return; |
|
| 262 | + | } |
|
| 263 | + | ||
| 264 | + | // Not the callback path |
|
| 265 | + | res.writeHead(404); |
|
| 266 | + | res.end("Not found"); |
|
| 267 | + | }); |
|
| 268 | + | ||
| 269 | + | server.on("error", (err: NodeJS.ErrnoException) => { |
|
| 270 | + | if (timeoutId) clearTimeout(timeoutId); |
|
| 271 | + | if (err.code === "EADDRINUSE") { |
|
| 272 | + | resolve({ |
|
| 273 | + | success: false, |
|
| 274 | + | error: `Port ${port} is already in use. Please close the application using that port and try again.`, |
|
| 275 | + | }); |
|
| 276 | + | } else { |
|
| 277 | + | resolve({ |
|
| 278 | + | success: false, |
|
| 279 | + | error: `Server error: ${err.message}`, |
|
| 280 | + | }); |
|
| 281 | + | } |
|
| 282 | + | }); |
|
| 283 | + | ||
| 284 | + | server.listen(port, "127.0.0.1"); |
|
| 285 | + | ||
| 286 | + | // Timeout after 5 minutes |
|
| 287 | + | timeoutId = setTimeout(() => { |
|
| 288 | + | server.close(() => { |
|
| 289 | + | resolve({ |
|
| 290 | + | success: false, |
|
| 291 | + | error: "Timeout waiting for OAuth callback. Please try again.", |
|
| 292 | + | }); |
|
| 293 | + | }); |
|
| 294 | + | }, CALLBACK_TIMEOUT_MS); |
|
| 295 | + | }); |
|
| 296 | + | } |
| 4 | 4 | import { authCommand } from "./commands/auth"; |
|
| 5 | 5 | import { initCommand } from "./commands/init"; |
|
| 6 | 6 | import { injectCommand } from "./commands/inject"; |
|
| 7 | + | import { loginCommand } from "./commands/login"; |
|
| 7 | 8 | import { publishCommand } from "./commands/publish"; |
|
| 8 | 9 | import { syncCommand } from "./commands/sync"; |
|
| 9 | 10 | ||
| 38 | 39 | auth: authCommand, |
|
| 39 | 40 | init: initCommand, |
|
| 40 | 41 | inject: injectCommand, |
|
| 42 | + | login: loginCommand, |
|
| 41 | 43 | publish: publishCommand, |
|
| 42 | 44 | sync: syncCommand, |
|
| 43 | 45 | }, |
|
| 1 | - | import { AtpAgent } from "@atproto/api"; |
|
| 1 | + | import { Agent, AtpAgent } from "@atproto/api"; |
|
| 2 | 2 | import * as mimeTypes from "mime-types"; |
|
| 3 | 3 | import * as fs from "node:fs/promises"; |
|
| 4 | 4 | import * as path from "node:path"; |
|
| 5 | 5 | import { stripMarkdownForText } from "./markdown"; |
|
| 6 | + | import { getOAuthClient } from "./oauth-client"; |
|
| 6 | 7 | import type { |
|
| 7 | 8 | BlobObject, |
|
| 8 | 9 | BlogPost, |
|
| 10 | 11 | PublisherConfig, |
|
| 11 | 12 | StrongRef, |
|
| 12 | 13 | } from "./types"; |
|
| 14 | + | import { isAppPasswordCredentials, isOAuthCredentials } from "./types"; |
|
| 13 | 15 | ||
| 14 | 16 | async function fileExists(filePath: string): Promise<boolean> { |
|
| 15 | 17 | try { |
|
| 20 | 22 | } |
|
| 21 | 23 | } |
|
| 22 | 24 | ||
| 25 | + | /** |
|
| 26 | + | * Resolve a handle to a DID |
|
| 27 | + | */ |
|
| 28 | + | export async function resolveHandleToDid(handle: string): Promise<string> { |
|
| 29 | + | if (handle.startsWith("did:")) { |
|
| 30 | + | return handle; |
|
| 31 | + | } |
|
| 32 | + | ||
| 33 | + | // Try to resolve handle via Bluesky API |
|
| 34 | + | const resolveUrl = `https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(handle)}`; |
|
| 35 | + | const resolveResponse = await fetch(resolveUrl); |
|
| 36 | + | if (!resolveResponse.ok) { |
|
| 37 | + | throw new Error("Could not resolve handle"); |
|
| 38 | + | } |
|
| 39 | + | const resolveData = (await resolveResponse.json()) as { did: string }; |
|
| 40 | + | return resolveData.did; |
|
| 41 | + | } |
|
| 42 | + | ||
| 23 | 43 | export async function resolveHandleToPDS(handle: string): Promise<string> { |
|
| 24 | 44 | // First, resolve the handle to a DID |
|
| 25 | - | let did: string; |
|
| 26 | - | ||
| 27 | - | if (handle.startsWith("did:")) { |
|
| 28 | - | did = handle; |
|
| 29 | - | } else { |
|
| 30 | - | // Try to resolve handle via Bluesky API |
|
| 31 | - | const resolveUrl = `https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(handle)}`; |
|
| 32 | - | const resolveResponse = await fetch(resolveUrl); |
|
| 33 | - | if (!resolveResponse.ok) { |
|
| 34 | - | throw new Error("Could not resolve handle"); |
|
| 35 | - | } |
|
| 36 | - | const resolveData = (await resolveResponse.json()) as { did: string }; |
|
| 37 | - | did = resolveData.did; |
|
| 38 | - | } |
|
| 45 | + | const did = await resolveHandleToDid(handle); |
|
| 39 | 46 | ||
| 40 | 47 | // Now resolve the DID to get the PDS URL from the DID document |
|
| 41 | 48 | let pdsUrl: string | undefined; |
|
| 90 | 97 | } |
|
| 91 | 98 | ||
| 92 | 99 | export async function createAgent(credentials: Credentials): Promise<AtpAgent> { |
|
| 100 | + | if (isOAuthCredentials(credentials)) { |
|
| 101 | + | // OAuth flow - restore session from stored tokens |
|
| 102 | + | const client = await getOAuthClient(); |
|
| 103 | + | try { |
|
| 104 | + | const oauthSession = await client.restore(credentials.did); |
|
| 105 | + | // Wrap the OAuth session in an Agent which provides the atproto API |
|
| 106 | + | const agent = new Agent(oauthSession) as unknown as AtpAgent; |
|
| 107 | + | ||
| 108 | + | // The Agent class doesn't have session.did like AtpAgent does |
|
| 109 | + | // We need to set up a compatible session object for the rest of our code |
|
| 110 | + | agent.session = { |
|
| 111 | + | did: oauthSession.did, |
|
| 112 | + | handle: credentials.handle, |
|
| 113 | + | accessJwt: "", |
|
| 114 | + | refreshJwt: "", |
|
| 115 | + | active: true, |
|
| 116 | + | }; |
|
| 117 | + | ||
| 118 | + | return agent; |
|
| 119 | + | } catch (error) { |
|
| 120 | + | if (error instanceof Error) { |
|
| 121 | + | // Check for common OAuth errors |
|
| 122 | + | if ( |
|
| 123 | + | error.message.includes("expired") || |
|
| 124 | + | error.message.includes("revoked") |
|
| 125 | + | ) { |
|
| 126 | + | throw new Error( |
|
| 127 | + | `OAuth session expired or revoked. Please run 'sequoia login' to re-authenticate.`, |
|
| 128 | + | ); |
|
| 129 | + | } |
|
| 130 | + | } |
|
| 131 | + | throw error; |
|
| 132 | + | } |
|
| 133 | + | } |
|
| 134 | + | ||
| 135 | + | // App password flow |
|
| 136 | + | if (!isAppPasswordCredentials(credentials)) { |
|
| 137 | + | throw new Error("Invalid credential type"); |
|
| 138 | + | } |
|
| 93 | 139 | const agent = new AtpAgent({ service: credentials.pdsUrl }); |
|
| 94 | 140 | ||
| 95 | 141 | await agent.login({ |
|
| 1 | 1 | import * as fs from "node:fs/promises"; |
|
| 2 | 2 | import * as os from "node:os"; |
|
| 3 | 3 | import * as path from "node:path"; |
|
| 4 | - | import type { Credentials } from "./types"; |
|
| 4 | + | import { getOAuthSession, listOAuthSessions } from "./oauth-store"; |
|
| 5 | + | import type { |
|
| 6 | + | AppPasswordCredentials, |
|
| 7 | + | Credentials, |
|
| 8 | + | LegacyCredentials, |
|
| 9 | + | OAuthCredentials, |
|
| 10 | + | } from "./types"; |
|
| 5 | 11 | ||
| 6 | 12 | const CONFIG_DIR = path.join(os.homedir(), ".config", "sequoia"); |
|
| 7 | 13 | const CREDENTIALS_FILE = path.join(CONFIG_DIR, "credentials.json"); |
|
| 8 | 14 | ||
| 9 | - | // Stored credentials keyed by identifier |
|
| 10 | - | type CredentialsStore = Record<string, Credentials>; |
|
| 15 | + | // Stored credentials keyed by identifier (can be legacy or typed) |
|
| 16 | + | type CredentialsStore = Record< |
|
| 17 | + | string, |
|
| 18 | + | AppPasswordCredentials | LegacyCredentials |
|
| 19 | + | >; |
|
| 11 | 20 | ||
| 12 | 21 | async function fileExists(filePath: string): Promise<boolean> { |
|
| 13 | 22 | try { |
|
| 19 | 28 | } |
|
| 20 | 29 | ||
| 21 | 30 | /** |
|
| 22 | - | * Load all stored credentials |
|
| 31 | + | * Normalize credentials to have explicit type |
|
| 23 | 32 | */ |
|
| 33 | + | function normalizeCredentials( |
|
| 34 | + | creds: AppPasswordCredentials | LegacyCredentials, |
|
| 35 | + | ): AppPasswordCredentials { |
|
| 36 | + | // If it already has type, return as-is |
|
| 37 | + | if ("type" in creds && creds.type === "app-password") { |
|
| 38 | + | return creds; |
|
| 39 | + | } |
|
| 40 | + | // Migrate legacy format |
|
| 41 | + | return { |
|
| 42 | + | type: "app-password", |
|
| 43 | + | pdsUrl: creds.pdsUrl, |
|
| 44 | + | identifier: creds.identifier, |
|
| 45 | + | password: creds.password, |
|
| 46 | + | }; |
|
| 47 | + | } |
|
| 48 | + | ||
| 24 | 49 | async function loadCredentialsStore(): Promise<CredentialsStore> { |
|
| 25 | 50 | if (!(await fileExists(CREDENTIALS_FILE))) { |
|
| 26 | 51 | return {}; |
|
| 32 | 57 | ||
| 33 | 58 | // Handle legacy single-credential format (migrate on read) |
|
| 34 | 59 | if (parsed.identifier && parsed.password) { |
|
| 35 | - | const legacy = parsed as Credentials; |
|
| 60 | + | const legacy = parsed as LegacyCredentials; |
|
| 36 | 61 | return { [legacy.identifier]: legacy }; |
|
| 37 | 62 | } |
|
| 38 | 63 | ||
| 52 | 77 | } |
|
| 53 | 78 | ||
| 54 | 79 | /** |
|
| 80 | + | * Try to load OAuth credentials for a given profile (DID or handle) |
|
| 81 | + | */ |
|
| 82 | + | async function tryLoadOAuthCredentials( |
|
| 83 | + | profile: string, |
|
| 84 | + | ): Promise<OAuthCredentials | null> { |
|
| 85 | + | // If it looks like a DID, try to get the session directly |
|
| 86 | + | if (profile.startsWith("did:")) { |
|
| 87 | + | const session = await getOAuthSession(profile); |
|
| 88 | + | if (session) { |
|
| 89 | + | return { |
|
| 90 | + | type: "oauth", |
|
| 91 | + | did: profile, |
|
| 92 | + | handle: profile, // We don't have the handle stored, use DID |
|
| 93 | + | pdsUrl: "https://bsky.social", // Will be resolved from DID doc |
|
| 94 | + | }; |
|
| 95 | + | } |
|
| 96 | + | } |
|
| 97 | + | ||
| 98 | + | // Otherwise, check all OAuth sessions to find a matching handle |
|
| 99 | + | // (This is a fallback - handle matching isn't perfect without storing handles) |
|
| 100 | + | const sessions = await listOAuthSessions(); |
|
| 101 | + | for (const did of sessions) { |
|
| 102 | + | // Could enhance this by storing handle with session, but for now |
|
| 103 | + | // just return null if profile isn't a DID |
|
| 104 | + | } |
|
| 105 | + | ||
| 106 | + | return null; |
|
| 107 | + | } |
|
| 108 | + | ||
| 109 | + | /** |
|
| 55 | 110 | * Load credentials for a specific identity or resolve which to use. |
|
| 56 | 111 | * |
|
| 57 | 112 | * Priority: |
|
| 58 | 113 | * 1. Full env vars (ATP_IDENTIFIER + ATP_APP_PASSWORD) |
|
| 59 | - | * 2. SEQUOIA_PROFILE env var - selects from stored credentials |
|
| 114 | + | * 2. SEQUOIA_PROFILE env var - selects from stored credentials (app-password or OAuth DID) |
|
| 60 | 115 | * 3. projectIdentity parameter (from sequoia.json) |
|
| 61 | - | * 4. If only one identity stored, use it |
|
| 116 | + | * 4. If only one identity stored (app-password or OAuth), use it |
|
| 62 | 117 | * 5. Return null (caller should prompt user) |
|
| 63 | 118 | */ |
|
| 64 | 119 | export async function loadCredentials( |
|
| 71 | 126 | ||
| 72 | 127 | if (envIdentifier && envPassword) { |
|
| 73 | 128 | return { |
|
| 129 | + | type: "app-password", |
|
| 74 | 130 | identifier: envIdentifier, |
|
| 75 | 131 | password: envPassword, |
|
| 76 | 132 | pdsUrl: envPdsUrl || "https://bsky.social", |
|
| 78 | 134 | } |
|
| 79 | 135 | ||
| 80 | 136 | const store = await loadCredentialsStore(); |
|
| 81 | - | const identifiers = Object.keys(store); |
|
| 82 | - | ||
| 83 | - | if (identifiers.length === 0) { |
|
| 84 | - | return null; |
|
| 85 | - | } |
|
| 137 | + | const appPasswordIds = Object.keys(store); |
|
| 138 | + | const oauthDids = await listOAuthSessions(); |
|
| 86 | 139 | ||
| 87 | 140 | // 2. SEQUOIA_PROFILE env var |
|
| 88 | 141 | const profileEnv = process.env.SEQUOIA_PROFILE; |
|
| 89 | - | if (profileEnv && store[profileEnv]) { |
|
| 90 | - | return store[profileEnv]; |
|
| 142 | + | if (profileEnv) { |
|
| 143 | + | // Try app-password credentials first |
|
| 144 | + | if (store[profileEnv]) { |
|
| 145 | + | return normalizeCredentials(store[profileEnv]); |
|
| 146 | + | } |
|
| 147 | + | // Try OAuth session (profile could be a DID) |
|
| 148 | + | const oauth = await tryLoadOAuthCredentials(profileEnv); |
|
| 149 | + | if (oauth) { |
|
| 150 | + | return oauth; |
|
| 151 | + | } |
|
| 91 | 152 | } |
|
| 92 | 153 | ||
| 93 | 154 | // 3. Project-specific identity (from sequoia.json) |
|
| 94 | - | if (projectIdentity && store[projectIdentity]) { |
|
| 95 | - | return store[projectIdentity]; |
|
| 155 | + | if (projectIdentity) { |
|
| 156 | + | if (store[projectIdentity]) { |
|
| 157 | + | return normalizeCredentials(store[projectIdentity]); |
|
| 158 | + | } |
|
| 159 | + | const oauth = await tryLoadOAuthCredentials(projectIdentity); |
|
| 160 | + | if (oauth) { |
|
| 161 | + | return oauth; |
|
| 162 | + | } |
|
| 96 | 163 | } |
|
| 97 | 164 | ||
| 98 | - | // 4. If only one identity, use it |
|
| 99 | - | if (identifiers.length === 1 && identifiers[0]) { |
|
| 100 | - | return store[identifiers[0]] ?? null; |
|
| 165 | + | // 4. If only one identity total, use it |
|
| 166 | + | const totalIdentities = appPasswordIds.length + oauthDids.length; |
|
| 167 | + | if (totalIdentities === 1) { |
|
| 168 | + | if (appPasswordIds.length === 1 && appPasswordIds[0]) { |
|
| 169 | + | return normalizeCredentials(store[appPasswordIds[0]]!); |
|
| 170 | + | } |
|
| 171 | + | if (oauthDids.length === 1 && oauthDids[0]) { |
|
| 172 | + | const session = await getOAuthSession(oauthDids[0]); |
|
| 173 | + | if (session) { |
|
| 174 | + | return { |
|
| 175 | + | type: "oauth", |
|
| 176 | + | did: oauthDids[0], |
|
| 177 | + | handle: oauthDids[0], |
|
| 178 | + | pdsUrl: "https://bsky.social", |
|
| 179 | + | }; |
|
| 180 | + | } |
|
| 181 | + | } |
|
| 101 | 182 | } |
|
| 102 | 183 | ||
| 103 | - | // Multiple identities exist but none selected |
|
| 184 | + | // Multiple identities exist but none selected, or no identities |
|
| 104 | 185 | return null; |
|
| 105 | 186 | } |
|
| 106 | 187 | ||
| 107 | 188 | /** |
|
| 108 | - | * Get a specific identity by identifier |
|
| 189 | + | * Get a specific identity by identifier (app-password only) |
|
| 109 | 190 | */ |
|
| 110 | 191 | export async function getCredentials( |
|
| 111 | 192 | identifier: string, |
|
| 112 | - | ): Promise<Credentials | null> { |
|
| 193 | + | ): Promise<AppPasswordCredentials | null> { |
|
| 113 | 194 | const store = await loadCredentialsStore(); |
|
| 114 | - | return store[identifier] || null; |
|
| 195 | + | const creds = store[identifier]; |
|
| 196 | + | if (!creds) return null; |
|
| 197 | + | return normalizeCredentials(creds); |
|
| 115 | 198 | } |
|
| 116 | 199 | ||
| 117 | 200 | /** |
|
| 118 | - | * List all stored identities |
|
| 201 | + | * List all stored app-password identities |
|
| 119 | 202 | */ |
|
| 120 | 203 | export async function listCredentials(): Promise<string[]> { |
|
| 121 | 204 | const store = await loadCredentialsStore(); |
|
| 123 | 206 | } |
|
| 124 | 207 | ||
| 125 | 208 | /** |
|
| 126 | - | * Save credentials for an identity (adds or updates) |
|
| 209 | + | * List all credentials (both app-password and OAuth) |
|
| 210 | + | */ |
|
| 211 | + | export async function listAllCredentials(): Promise< |
|
| 212 | + | Array<{ id: string; type: "app-password" | "oauth" }> |
|
| 213 | + | > { |
|
| 214 | + | const store = await loadCredentialsStore(); |
|
| 215 | + | const oauthDids = await listOAuthSessions(); |
|
| 216 | + | ||
| 217 | + | const result: Array<{ id: string; type: "app-password" | "oauth" }> = []; |
|
| 218 | + | ||
| 219 | + | for (const id of Object.keys(store)) { |
|
| 220 | + | result.push({ id, type: "app-password" }); |
|
| 221 | + | } |
|
| 222 | + | ||
| 223 | + | for (const did of oauthDids) { |
|
| 224 | + | result.push({ id: did, type: "oauth" }); |
|
| 225 | + | } |
|
| 226 | + | ||
| 227 | + | return result; |
|
| 228 | + | } |
|
| 229 | + | ||
| 230 | + | /** |
|
| 231 | + | * Save app-password credentials for an identity (adds or updates) |
|
| 127 | 232 | */ |
|
| 128 | - | export async function saveCredentials(credentials: Credentials): Promise<void> { |
|
| 233 | + | export async function saveCredentials( |
|
| 234 | + | credentials: AppPasswordCredentials, |
|
| 235 | + | ): Promise<void> { |
|
| 129 | 236 | const store = await loadCredentialsStore(); |
|
| 130 | 237 | store[credentials.identifier] = credentials; |
|
| 131 | 238 | await saveCredentialsStore(store); |
|
| 1 | + | import { |
|
| 2 | + | NodeOAuthClient, |
|
| 3 | + | type NodeOAuthClientOptions, |
|
| 4 | + | } from "@atproto/oauth-client-node"; |
|
| 5 | + | import { sessionStore, stateStore } from "./oauth-store"; |
|
| 6 | + | ||
| 7 | + | const CALLBACK_PORT = 4000; |
|
| 8 | + | const CALLBACK_HOST = "127.0.0.1"; |
|
| 9 | + | const CALLBACK_URL = `http://${CALLBACK_HOST}:${CALLBACK_PORT}/oauth/callback`; |
|
| 10 | + | ||
| 11 | + | // OAuth scope for Sequoia CLI - includes atproto base scope plus our collections |
|
| 12 | + | const OAUTH_SCOPE = |
|
| 13 | + | "atproto repo:site.standard.document repo:site.standard.publication repo:app.bsky.feed.post blob:*/*"; |
|
| 14 | + | ||
| 15 | + | let oauthClient: NodeOAuthClient | null = null; |
|
| 16 | + | ||
| 17 | + | // Simple lock implementation for CLI (single process, no contention) |
|
| 18 | + | // This prevents the "No lock mechanism provided" warning |
|
| 19 | + | const locks = new Map<string, Promise<void>>(); |
|
| 20 | + | ||
| 21 | + | async function requestLock(key: string, fn: () => Promise<void>): Promise<void> { |
|
| 22 | + | // Wait for any existing lock on this key |
|
| 23 | + | while (locks.has(key)) { |
|
| 24 | + | await locks.get(key); |
|
| 25 | + | } |
|
| 26 | + | ||
| 27 | + | // Create our lock |
|
| 28 | + | let resolve: () => void; |
|
| 29 | + | const lockPromise = new Promise<void>((r) => { |
|
| 30 | + | resolve = r; |
|
| 31 | + | }); |
|
| 32 | + | locks.set(key, lockPromise); |
|
| 33 | + | ||
| 34 | + | try { |
|
| 35 | + | await fn(); |
|
| 36 | + | } finally { |
|
| 37 | + | locks.delete(key); |
|
| 38 | + | resolve!(); |
|
| 39 | + | } |
|
| 40 | + | } |
|
| 41 | + | ||
| 42 | + | /** |
|
| 43 | + | * Get or create the OAuth client singleton |
|
| 44 | + | */ |
|
| 45 | + | export async function getOAuthClient(): Promise<NodeOAuthClient> { |
|
| 46 | + | if (oauthClient) { |
|
| 47 | + | return oauthClient; |
|
| 48 | + | } |
|
| 49 | + | ||
| 50 | + | // Build client_id with required parameters |
|
| 51 | + | const clientIdParams = new URLSearchParams(); |
|
| 52 | + | clientIdParams.append("redirect_uri", CALLBACK_URL); |
|
| 53 | + | clientIdParams.append("scope", OAUTH_SCOPE); |
|
| 54 | + | ||
| 55 | + | const clientOptions: NodeOAuthClientOptions = { |
|
| 56 | + | clientMetadata: { |
|
| 57 | + | client_id: `http://localhost?${clientIdParams.toString()}`, |
|
| 58 | + | client_name: "Sequoia CLI", |
|
| 59 | + | client_uri: "https://github.com/stevedylandev/sequoia", |
|
| 60 | + | redirect_uris: [CALLBACK_URL], |
|
| 61 | + | grant_types: ["authorization_code", "refresh_token"], |
|
| 62 | + | response_types: ["code"], |
|
| 63 | + | token_endpoint_auth_method: "none", |
|
| 64 | + | application_type: "web", |
|
| 65 | + | scope: OAUTH_SCOPE, |
|
| 66 | + | dpop_bound_access_tokens: false, |
|
| 67 | + | }, |
|
| 68 | + | stateStore, |
|
| 69 | + | sessionStore, |
|
| 70 | + | // Configure identity resolution |
|
| 71 | + | plcDirectoryUrl: "https://plc.directory", |
|
| 72 | + | // Provide lock mechanism to prevent warning |
|
| 73 | + | requestLock, |
|
| 74 | + | }; |
|
| 75 | + | ||
| 76 | + | oauthClient = new NodeOAuthClient(clientOptions); |
|
| 77 | + | ||
| 78 | + | return oauthClient; |
|
| 79 | + | } |
|
| 80 | + | ||
| 81 | + | export function getOAuthScope(): string { |
|
| 82 | + | return OAUTH_SCOPE; |
|
| 83 | + | } |
|
| 84 | + | ||
| 85 | + | export function getCallbackUrl(): string { |
|
| 86 | + | return CALLBACK_URL; |
|
| 87 | + | } |
|
| 88 | + | ||
| 89 | + | export function getCallbackPort(): number { |
|
| 90 | + | return CALLBACK_PORT; |
|
| 91 | + | } |
| 1 | + | import * as fs from "node:fs/promises"; |
|
| 2 | + | import * as os from "node:os"; |
|
| 3 | + | import * as path from "node:path"; |
|
| 4 | + | import type { |
|
| 5 | + | NodeSavedSession, |
|
| 6 | + | NodeSavedSessionStore, |
|
| 7 | + | NodeSavedState, |
|
| 8 | + | NodeSavedStateStore, |
|
| 9 | + | } from "@atproto/oauth-client-node"; |
|
| 10 | + | ||
| 11 | + | const CONFIG_DIR = path.join(os.homedir(), ".config", "sequoia"); |
|
| 12 | + | const OAUTH_FILE = path.join(CONFIG_DIR, "oauth.json"); |
|
| 13 | + | ||
| 14 | + | interface OAuthStore { |
|
| 15 | + | states: Record<string, NodeSavedState>; |
|
| 16 | + | sessions: Record<string, NodeSavedSession>; |
|
| 17 | + | } |
|
| 18 | + | ||
| 19 | + | async function fileExists(filePath: string): Promise<boolean> { |
|
| 20 | + | try { |
|
| 21 | + | await fs.access(filePath); |
|
| 22 | + | return true; |
|
| 23 | + | } catch { |
|
| 24 | + | return false; |
|
| 25 | + | } |
|
| 26 | + | } |
|
| 27 | + | ||
| 28 | + | async function loadOAuthStore(): Promise<OAuthStore> { |
|
| 29 | + | if (!(await fileExists(OAUTH_FILE))) { |
|
| 30 | + | return { states: {}, sessions: {} }; |
|
| 31 | + | } |
|
| 32 | + | ||
| 33 | + | try { |
|
| 34 | + | const content = await fs.readFile(OAUTH_FILE, "utf-8"); |
|
| 35 | + | return JSON.parse(content) as OAuthStore; |
|
| 36 | + | } catch { |
|
| 37 | + | return { states: {}, sessions: {} }; |
|
| 38 | + | } |
|
| 39 | + | } |
|
| 40 | + | ||
| 41 | + | async function saveOAuthStore(store: OAuthStore): Promise<void> { |
|
| 42 | + | await fs.mkdir(CONFIG_DIR, { recursive: true }); |
|
| 43 | + | await fs.writeFile(OAUTH_FILE, JSON.stringify(store, null, 2)); |
|
| 44 | + | await fs.chmod(OAUTH_FILE, 0o600); |
|
| 45 | + | } |
|
| 46 | + | ||
| 47 | + | /** |
|
| 48 | + | * State store for PKCE flow (temporary, used during auth) |
|
| 49 | + | */ |
|
| 50 | + | export const stateStore: NodeSavedStateStore = { |
|
| 51 | + | async set(key: string, state: NodeSavedState): Promise<void> { |
|
| 52 | + | const store = await loadOAuthStore(); |
|
| 53 | + | store.states[key] = state; |
|
| 54 | + | await saveOAuthStore(store); |
|
| 55 | + | }, |
|
| 56 | + | ||
| 57 | + | async get(key: string): Promise<NodeSavedState | undefined> { |
|
| 58 | + | const store = await loadOAuthStore(); |
|
| 59 | + | return store.states[key]; |
|
| 60 | + | }, |
|
| 61 | + | ||
| 62 | + | async del(key: string): Promise<void> { |
|
| 63 | + | const store = await loadOAuthStore(); |
|
| 64 | + | delete store.states[key]; |
|
| 65 | + | await saveOAuthStore(store); |
|
| 66 | + | }, |
|
| 67 | + | }; |
|
| 68 | + | ||
| 69 | + | /** |
|
| 70 | + | * Session store for OAuth tokens (persistent) |
|
| 71 | + | */ |
|
| 72 | + | export const sessionStore: NodeSavedSessionStore = { |
|
| 73 | + | async set(sub: string, session: NodeSavedSession): Promise<void> { |
|
| 74 | + | const store = await loadOAuthStore(); |
|
| 75 | + | store.sessions[sub] = session; |
|
| 76 | + | await saveOAuthStore(store); |
|
| 77 | + | }, |
|
| 78 | + | ||
| 79 | + | async get(sub: string): Promise<NodeSavedSession | undefined> { |
|
| 80 | + | const store = await loadOAuthStore(); |
|
| 81 | + | return store.sessions[sub]; |
|
| 82 | + | }, |
|
| 83 | + | ||
| 84 | + | async del(sub: string): Promise<void> { |
|
| 85 | + | const store = await loadOAuthStore(); |
|
| 86 | + | delete store.sessions[sub]; |
|
| 87 | + | await saveOAuthStore(store); |
|
| 88 | + | }, |
|
| 89 | + | }; |
|
| 90 | + | ||
| 91 | + | /** |
|
| 92 | + | * List all stored OAuth session DIDs |
|
| 93 | + | */ |
|
| 94 | + | export async function listOAuthSessions(): Promise<string[]> { |
|
| 95 | + | const store = await loadOAuthStore(); |
|
| 96 | + | return Object.keys(store.sessions); |
|
| 97 | + | } |
|
| 98 | + | ||
| 99 | + | /** |
|
| 100 | + | * Get an OAuth session by DID |
|
| 101 | + | */ |
|
| 102 | + | export async function getOAuthSession( |
|
| 103 | + | did: string, |
|
| 104 | + | ): Promise<NodeSavedSession | undefined> { |
|
| 105 | + | const store = await loadOAuthStore(); |
|
| 106 | + | return store.sessions[did]; |
|
| 107 | + | } |
|
| 108 | + | ||
| 109 | + | /** |
|
| 110 | + | * Delete an OAuth session by DID |
|
| 111 | + | */ |
|
| 112 | + | export async function deleteOAuthSession(did: string): Promise<boolean> { |
|
| 113 | + | const store = await loadOAuthStore(); |
|
| 114 | + | if (!store.sessions[did]) { |
|
| 115 | + | return false; |
|
| 116 | + | } |
|
| 117 | + | delete store.sessions[did]; |
|
| 118 | + | await saveOAuthStore(store); |
|
| 119 | + | return true; |
|
| 120 | + | } |
|
| 121 | + | ||
| 122 | + | export function getOAuthStorePath(): string { |
|
| 123 | + | return OAUTH_FILE; |
|
| 124 | + | } |
| 37 | 37 | bluesky?: BlueskyConfig; // Optional Bluesky posting configuration |
|
| 38 | 38 | } |
|
| 39 | 39 | ||
| 40 | - | export interface Credentials { |
|
| 40 | + | // Legacy credentials format (for backward compatibility during migration) |
|
| 41 | + | export interface LegacyCredentials { |
|
| 41 | 42 | pdsUrl: string; |
|
| 42 | 43 | identifier: string; |
|
| 43 | 44 | password: string; |
|
| 45 | + | } |
|
| 46 | + | ||
| 47 | + | // App password credentials (explicit type) |
|
| 48 | + | export interface AppPasswordCredentials { |
|
| 49 | + | type: "app-password"; |
|
| 50 | + | pdsUrl: string; |
|
| 51 | + | identifier: string; |
|
| 52 | + | password: string; |
|
| 53 | + | } |
|
| 54 | + | ||
| 55 | + | // OAuth credentials (references stored OAuth session) |
|
| 56 | + | export interface OAuthCredentials { |
|
| 57 | + | type: "oauth"; |
|
| 58 | + | did: string; |
|
| 59 | + | handle: string; |
|
| 60 | + | pdsUrl: string; |
|
| 61 | + | } |
|
| 62 | + | ||
| 63 | + | // Union type for all credential types |
|
| 64 | + | export type Credentials = AppPasswordCredentials | OAuthCredentials; |
|
| 65 | + | ||
| 66 | + | // Helper to check credential type |
|
| 67 | + | export function isOAuthCredentials( |
|
| 68 | + | creds: Credentials, |
|
| 69 | + | ): creds is OAuthCredentials { |
|
| 70 | + | return creds.type === "oauth"; |
|
| 71 | + | } |
|
| 72 | + | ||
| 73 | + | export function isAppPasswordCredentials( |
|
| 74 | + | creds: Credentials, |
|
| 75 | + | ): creds is AppPasswordCredentials { |
|
| 76 | + | return creds.type === "app-password"; |
|
| 44 | 77 | } |
|
| 45 | 78 | ||
| 46 | 79 | export interface PostFrontmatter { |