A CLI for publishing standard.site documents to ATProto

feat: initial oauth implementation

authored by stevedylan.dev and committed by tangled.org 960999bb 2dd2e1c3

+812 -44
+67 -1
bun.lock
··· 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
+3 -1
packages/cli/package.json
··· 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 }
+1
packages/cli/src/commands/auth.ts
··· 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,
+296
packages/cli/src/commands/login.ts
··· 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 + }
+2
packages/cli/src/index.ts
··· 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 },
+61 -15
packages/cli/src/lib/atproto.ts
··· 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({
+133 -26
packages/cli/src/lib/credentials.ts
··· 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);
+91
packages/cli/src/lib/oauth-client.ts
··· 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 + }
+124
packages/cli/src/lib/oauth-store.ts
··· 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 + }
+34 -1
packages/cli/src/lib/types.ts
··· 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 {