- Adds a new
logincommand that can be used to login with OAuth instead of App Password - Stores credentials and refreshes token as it expires
- Updated docs to reflect login flow
+67
-1
bun.lock
+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
+19
-1
docs/docs/pages/cli-reference.mdx
+19
-1
docs/docs/pages/cli-reference.mdx
···
1
1
# CLI Reference
2
2
3
+
## `login`
4
+
5
+
```bash [Terminal]
6
+
sequoia login
7
+
> Login with OAuth (browser-based authentication)
8
+
9
+
OPTIONS:
10
+
--logout <str> - Remove OAuth session for a specific DID [optional]
11
+
12
+
FLAGS:
13
+
--list - List all stored OAuth sessions [optional]
14
+
--help, -h - show help [optional]
15
+
```
16
+
17
+
OAuth is the recommended authentication method as it scopes permissions and refreshes tokens automatically.
18
+
3
19
## `auth`
4
20
5
21
```bash [Terminal]
6
22
sequoia auth
7
-
> Authenticate with your ATProto PDS
23
+
> Authenticate with your ATProto PDS using an app password
8
24
9
25
OPTIONS:
10
26
--logout <str> - Remove credentials for a specific identity (or all if only one exists) [optional]
···
13
29
--list - List all stored identities [optional]
14
30
--help, -h - show help [optional]
15
31
```
32
+
33
+
Use this as an alternative to `login` when OAuth isn't available or for CI environments.
16
34
17
35
## `init`
18
36
+9
-7
docs/docs/pages/quickstart.mdx
+9
-7
docs/docs/pages/quickstart.mdx
···
31
31
sequoia
32
32
```
33
33
34
-
### Authorize
35
-
36
-
In order for Sequoia to publish or update records on your PDS, you need to authorize it with your ATProto handle and an app password.
34
+
### Login
37
35
38
-
:::tip
39
-
You can create an app password [here](https://bsky.app/settings/app-passwords)
40
-
:::
36
+
In order for Sequoia to publish or update records on your PDS, you need to authenticate with your ATProto account.
41
37
42
38
```bash [Terminal]
43
-
sequoia auth
39
+
sequoia login
44
40
```
41
+
42
+
This will open your browser to complete OAuth authentication, and your sessions will refresh automatically as you use the CLI.
43
+
44
+
:::tip
45
+
Alternatively, you can use `sequoia auth` to authenticate with an [app password](https://bsky.app/settings/app-passwords) instead of OAuth.
46
+
:::
45
47
46
48
### Initialize
47
49
docs/docs/public/icon-dark.png
docs/docs/public/icon-dark.png
This is a binary file and will not be displayed.
docs/docs/public/og.png
docs/docs/public/og.png
This is a binary file and will not be displayed.
+3
-1
packages/cli/package.json
+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
+1
packages/cli/src/commands/auth.ts
+4
-1
packages/cli/src/commands/init.ts
+4
-1
packages/cli/src/commands/init.ts
···
287
287
defaultValue: "7",
288
288
placeholder: "7",
289
289
validate: (value) => {
290
-
const num = parseInt(value, 10);
290
+
if (!value) {
291
+
return "Please enter a number";
292
+
}
293
+
const num = Number.parseInt(value, 10);
291
294
if (Number.isNaN(num) || num < 1) {
292
295
return "Please enter a positive number";
293
296
}
+303
packages/cli/src/commands/login.ts
+303
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
+
getOAuthClient,
8
+
getOAuthScope,
9
+
} from "../lib/oauth-client";
10
+
import {
11
+
deleteOAuthSession,
12
+
getOAuthStorePath,
13
+
listOAuthSessions,
14
+
} from "../lib/oauth-store";
15
+
import { exitOnCancel } from "../lib/prompts";
16
+
17
+
const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
18
+
19
+
export const loginCommand = command({
20
+
name: "login",
21
+
description: "Login with OAuth (browser-based authentication)",
22
+
args: {
23
+
logout: option({
24
+
long: "logout",
25
+
description: "Remove OAuth session for a specific DID",
26
+
type: optional(string),
27
+
}),
28
+
list: flag({
29
+
long: "list",
30
+
description: "List all stored OAuth sessions",
31
+
}),
32
+
},
33
+
handler: async ({ logout, list }) => {
34
+
// List sessions
35
+
if (list) {
36
+
const sessions = await listOAuthSessions();
37
+
if (sessions.length === 0) {
38
+
log.info("No OAuth sessions stored");
39
+
} else {
40
+
log.info("OAuth sessions:");
41
+
for (const did of sessions) {
42
+
console.log(` - ${did}`);
43
+
}
44
+
}
45
+
return;
46
+
}
47
+
48
+
// Logout
49
+
if (logout !== undefined) {
50
+
const did = logout || undefined;
51
+
52
+
if (!did) {
53
+
// No DID provided - show available and prompt
54
+
const sessions = await listOAuthSessions();
55
+
if (sessions.length === 0) {
56
+
log.info("No OAuth sessions found");
57
+
return;
58
+
}
59
+
if (sessions.length === 1) {
60
+
const deleted = await deleteOAuthSession(sessions[0]!);
61
+
if (deleted) {
62
+
log.success(`Removed OAuth session for ${sessions[0]}`);
63
+
}
64
+
return;
65
+
}
66
+
// Multiple sessions - prompt
67
+
const selected = exitOnCancel(
68
+
await select({
69
+
message: "Select session to remove:",
70
+
options: sessions.map((d) => ({ value: d, label: d })),
71
+
}),
72
+
);
73
+
const deleted = await deleteOAuthSession(selected);
74
+
if (deleted) {
75
+
log.success(`Removed OAuth session for ${selected}`);
76
+
}
77
+
return;
78
+
}
79
+
80
+
const deleted = await deleteOAuthSession(did);
81
+
if (deleted) {
82
+
log.success(`Removed OAuth session for ${did}`);
83
+
} else {
84
+
log.info(`No OAuth session found for ${did}`);
85
+
}
86
+
return;
87
+
}
88
+
89
+
// OAuth login flow
90
+
note(
91
+
"OAuth login will open your browser to authenticate.\n\n" +
92
+
"This is more secure than app passwords and tokens refresh automatically.",
93
+
"OAuth Login",
94
+
);
95
+
96
+
const handle = exitOnCancel(
97
+
await text({
98
+
message: "Handle or DID:",
99
+
placeholder: "yourhandle.bsky.social",
100
+
}),
101
+
);
102
+
103
+
if (!handle) {
104
+
log.error("Handle is required");
105
+
process.exit(1);
106
+
}
107
+
108
+
const s = spinner();
109
+
s.start("Resolving identity...");
110
+
111
+
let did: string;
112
+
try {
113
+
did = await resolveHandleToDid(handle);
114
+
s.stop(`Identity resolved`);
115
+
} catch (error) {
116
+
s.stop("Failed to resolve identity");
117
+
if (error instanceof Error) {
118
+
log.error(`Error: ${error.message}`);
119
+
} else {
120
+
log.error(`Error: ${error}`);
121
+
}
122
+
process.exit(1);
123
+
}
124
+
125
+
s.start("Initializing OAuth...");
126
+
127
+
try {
128
+
const client = await getOAuthClient();
129
+
130
+
// Generate authorization URL using the resolved DID
131
+
const authUrl = await client.authorize(did, {
132
+
scope: getOAuthScope(),
133
+
});
134
+
135
+
log.info(`Login URL: ${authUrl}`);
136
+
137
+
s.message("Opening browser...");
138
+
139
+
// Try to open browser
140
+
let browserOpened = true;
141
+
try {
142
+
const open = (await import("open")).default;
143
+
await open(authUrl.toString());
144
+
} catch {
145
+
browserOpened = false;
146
+
}
147
+
148
+
s.message("Waiting for authentication...");
149
+
150
+
// Show URL info
151
+
if (!browserOpened) {
152
+
s.stop("Could not open browser automatically");
153
+
log.warn("Please open the following URL in your browser:");
154
+
log.info(authUrl.toString());
155
+
s.start("Waiting for authentication...");
156
+
}
157
+
158
+
// Start HTTP server to receive callback
159
+
const result = await waitForCallback();
160
+
161
+
if (!result.success) {
162
+
s.stop("Authentication failed");
163
+
log.error(result.error || "OAuth callback failed");
164
+
process.exit(1);
165
+
}
166
+
167
+
s.message("Completing authentication...");
168
+
169
+
// Exchange code for tokens
170
+
const { session } = await client.callback(
171
+
new URLSearchParams(result.params!),
172
+
);
173
+
174
+
// Try to get the handle for display (use the original handle input as fallback)
175
+
let displayName = handle;
176
+
try {
177
+
// The session should have the DID, we can use the original handle they entered
178
+
// or we could fetch the profile to get the current handle
179
+
displayName = handle.startsWith("did:") ? session.did : handle;
180
+
} catch {
181
+
displayName = session.did;
182
+
}
183
+
184
+
s.stop(`Logged in as ${displayName}`);
185
+
186
+
log.success(`OAuth session saved to ${getOAuthStorePath()}`);
187
+
log.info("Your session will refresh automatically when needed.");
188
+
189
+
// Exit cleanly - the OAuth client may have background processes
190
+
process.exit(0);
191
+
} catch (error) {
192
+
s.stop("OAuth login failed");
193
+
if (error instanceof Error) {
194
+
log.error(`Error: ${error.message}`);
195
+
} else {
196
+
log.error(`Error: ${error}`);
197
+
}
198
+
process.exit(1);
199
+
}
200
+
},
201
+
});
202
+
203
+
interface CallbackResult {
204
+
success: boolean;
205
+
params?: Record<string, string>;
206
+
error?: string;
207
+
}
208
+
209
+
function waitForCallback(): Promise<CallbackResult> {
210
+
return new Promise((resolve) => {
211
+
const port = getCallbackPort();
212
+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
213
+
214
+
const server = http.createServer((req, res) => {
215
+
const url = new URL(req.url || "/", `http://127.0.0.1:${port}`);
216
+
217
+
if (url.pathname === "/oauth/callback") {
218
+
const params: Record<string, string> = {};
219
+
url.searchParams.forEach((value, key) => {
220
+
params[key] = value;
221
+
});
222
+
223
+
// Clear the timeout
224
+
if (timeoutId) clearTimeout(timeoutId);
225
+
226
+
// Check for error
227
+
if (params.error) {
228
+
res.writeHead(200, { "Content-Type": "text/html" });
229
+
res.end(`
230
+
<html>
231
+
<head>
232
+
<link href="https://fonts.googleapis.com/css2?family=Josefin+Sans:wght@400;700&display=swap" rel="stylesheet">
233
+
</head>
234
+
<body style="background: #1A1A1A; color: #F5F3EF; font-family: 'Josefin Sans', system-ui; padding: 2rem; text-align: center; display: flex; flex-direction: column; justify-content: center; align-items: center; min-height: 100vh; margin: 0;">
235
+
<img src="https://sequoia.pub/icon-dark.png" alt="sequoia icon" style="width: 100px; height: 100px;" />
236
+
<h1 style="font-weight: 400;">Authentication Failed</h1>
237
+
<p>${params.error_description || params.error}</p>
238
+
<p>You can close this window.</p>
239
+
</body>
240
+
</html>
241
+
`);
242
+
server.close(() => {
243
+
resolve({
244
+
success: false,
245
+
error: params.error_description || params.error,
246
+
});
247
+
});
248
+
return;
249
+
}
250
+
251
+
// Success
252
+
res.writeHead(200, { "Content-Type": "text/html" });
253
+
res.end(`
254
+
<html>
255
+
<head>
256
+
<link href="https://fonts.googleapis.com/css2?family=Josefin+Sans:wght@400;700&display=swap" rel="stylesheet">
257
+
</head>
258
+
<body style="background: #1A1A1A; color: #F5F3EF; font-family: 'Josefin Sans', system-ui; padding: 2rem; text-align: center; display: flex; flex-direction: column; justify-content: center; align-items: center; min-height: 100vh; margin: 0;">
259
+
<img src="https://sequoia.pub/icon-dark.png" alt="sequoia icon" style="width: 100px; height: 100px;" />
260
+
<h1 style="font-weight: 400;">Authentication Successful</h1>
261
+
<p>You can close this window and return to the terminal.</p>
262
+
</body>
263
+
</html>
264
+
`);
265
+
server.close(() => {
266
+
resolve({ success: true, params });
267
+
});
268
+
return;
269
+
}
270
+
271
+
// Not the callback path
272
+
res.writeHead(404);
273
+
res.end("Not found");
274
+
});
275
+
276
+
server.on("error", (err: NodeJS.ErrnoException) => {
277
+
if (timeoutId) clearTimeout(timeoutId);
278
+
if (err.code === "EADDRINUSE") {
279
+
resolve({
280
+
success: false,
281
+
error: `Port ${port} is already in use. Please close the application using that port and try again.`,
282
+
});
283
+
} else {
284
+
resolve({
285
+
success: false,
286
+
error: `Server error: ${err.message}`,
287
+
});
288
+
}
289
+
});
290
+
291
+
server.listen(port, "127.0.0.1");
292
+
293
+
// Timeout after 5 minutes
294
+
timeoutId = setTimeout(() => {
295
+
server.close(() => {
296
+
resolve({
297
+
success: false,
298
+
error: "Timeout waiting for OAuth callback. Please try again.",
299
+
});
300
+
});
301
+
}, CALLBACK_TIMEOUT_MS);
302
+
});
303
+
}
+1
-1
packages/cli/src/commands/publish.ts
+1
-1
packages/cli/src/commands/publish.ts
···
209
209
let agent: Awaited<ReturnType<typeof createAgent>> | undefined;
210
210
try {
211
211
agent = await createAgent(credentials);
212
-
s.stop(`Logged in as ${agent.session?.handle}`);
212
+
s.stop(`Logged in as ${agent.did}`);
213
213
} catch (error) {
214
214
s.stop("Failed to login");
215
215
log.error(`Failed to login: ${error}`);
+1
-1
packages/cli/src/commands/sync.ts
+1
-1
packages/cli/src/commands/sync.ts
···
76
76
let agent: Awaited<ReturnType<typeof createAgent>> | undefined;
77
77
try {
78
78
agent = await createAgent(credentials);
79
-
s.stop(`Logged in as ${agent.session?.handle}`);
79
+
s.stop(`Logged in as ${agent.did}`);
80
80
} catch (error) {
81
81
s.stop("Failed to login");
82
82
log.error(`Failed to login: ${error}`);
+2
packages/cli/src/index.ts
+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
},
+83
-31
packages/cli/src/lib/atproto.ts
+83
-31
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";
15
+
16
+
/**
17
+
* Type guard to check if a record value is a DocumentRecord
18
+
*/
19
+
function isDocumentRecord(value: unknown): value is DocumentRecord {
20
+
if (!value || typeof value !== "object") return false;
21
+
const v = value as Record<string, unknown>;
22
+
return (
23
+
v.$type === "site.standard.document" &&
24
+
typeof v.title === "string" &&
25
+
typeof v.site === "string" &&
26
+
typeof v.path === "string" &&
27
+
typeof v.textContent === "string" &&
28
+
typeof v.publishedAt === "string"
29
+
);
30
+
}
13
31
14
32
async function fileExists(filePath: string): Promise<boolean> {
15
33
try {
···
20
38
}
21
39
}
22
40
41
+
/**
42
+
* Resolve a handle to a DID
43
+
*/
44
+
export async function resolveHandleToDid(handle: string): Promise<string> {
45
+
if (handle.startsWith("did:")) {
46
+
return handle;
47
+
}
48
+
49
+
// Try to resolve handle via Bluesky API
50
+
const resolveUrl = `https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(handle)}`;
51
+
const resolveResponse = await fetch(resolveUrl);
52
+
if (!resolveResponse.ok) {
53
+
throw new Error("Could not resolve handle");
54
+
}
55
+
const resolveData = (await resolveResponse.json()) as { did: string };
56
+
return resolveData.did;
57
+
}
58
+
23
59
export async function resolveHandleToPDS(handle: string): Promise<string> {
24
60
// 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
-
}
61
+
const did = await resolveHandleToDid(handle);
39
62
40
63
// Now resolve the DID to get the PDS URL from the DID document
41
64
let pdsUrl: string | undefined;
···
89
112
showInDiscover?: boolean;
90
113
}
91
114
92
-
export async function createAgent(credentials: Credentials): Promise<AtpAgent> {
115
+
export async function createAgent(credentials: Credentials): Promise<Agent> {
116
+
if (isOAuthCredentials(credentials)) {
117
+
// OAuth flow - restore session from stored tokens
118
+
const client = await getOAuthClient();
119
+
try {
120
+
const oauthSession = await client.restore(credentials.did);
121
+
// Wrap the OAuth session in an Agent which provides the atproto API
122
+
return new Agent(oauthSession);
123
+
} catch (error) {
124
+
if (error instanceof Error) {
125
+
// Check for common OAuth errors
126
+
if (
127
+
error.message.includes("expired") ||
128
+
error.message.includes("revoked")
129
+
) {
130
+
throw new Error(
131
+
`OAuth session expired or revoked. Please run 'sequoia login' to re-authenticate.`,
132
+
);
133
+
}
134
+
}
135
+
throw error;
136
+
}
137
+
}
138
+
139
+
// App password flow
140
+
if (!isAppPasswordCredentials(credentials)) {
141
+
throw new Error("Invalid credential type");
142
+
}
93
143
const agent = new AtpAgent({ service: credentials.pdsUrl });
94
144
95
145
await agent.login({
···
101
151
}
102
152
103
153
export async function uploadImage(
104
-
agent: AtpAgent,
154
+
agent: Agent,
105
155
imagePath: string,
106
156
): Promise<BlobObject | undefined> {
107
157
if (!(await fileExists(imagePath))) {
···
170
220
}
171
221
172
222
export async function createDocument(
173
-
agent: AtpAgent,
223
+
agent: Agent,
174
224
post: BlogPost,
175
225
config: PublisherConfig,
176
226
coverImage?: BlobObject,
···
213
263
}
214
264
215
265
const response = await agent.com.atproto.repo.createRecord({
216
-
repo: agent.session!.did,
266
+
repo: agent.did!,
217
267
collection: "site.standard.document",
218
268
record,
219
269
});
···
222
272
}
223
273
224
274
export async function updateDocument(
225
-
agent: AtpAgent,
275
+
agent: Agent,
226
276
post: BlogPost,
227
277
atUri: string,
228
278
config: PublisherConfig,
···
275
325
}
276
326
277
327
await agent.com.atproto.repo.putRecord({
278
-
repo: agent.session!.did,
328
+
repo: agent.did!,
279
329
collection: collection!,
280
330
rkey: rkey!,
281
331
record,
···
315
365
}
316
366
317
367
export async function listDocuments(
318
-
agent: AtpAgent,
368
+
agent: Agent,
319
369
publicationUri?: string,
320
370
): Promise<ListDocumentsResult[]> {
321
371
const documents: ListDocumentsResult[] = [];
···
323
373
324
374
do {
325
375
const response = await agent.com.atproto.repo.listRecords({
326
-
repo: agent.session!.did,
376
+
repo: agent.did!,
327
377
collection: "site.standard.document",
328
378
limit: 100,
329
379
cursor,
330
380
});
331
381
332
382
for (const record of response.data.records) {
333
-
const value = record.value as unknown as DocumentRecord;
383
+
if (!isDocumentRecord(record.value)) {
384
+
continue;
385
+
}
334
386
335
387
// If publicationUri is specified, only include documents from that publication
336
-
if (publicationUri && value.site !== publicationUri) {
388
+
if (publicationUri && record.value.site !== publicationUri) {
337
389
continue;
338
390
}
339
391
340
392
documents.push({
341
393
uri: record.uri,
342
394
cid: record.cid,
343
-
value,
395
+
value: record.value,
344
396
});
345
397
}
346
398
···
351
403
}
352
404
353
405
export async function createPublication(
354
-
agent: AtpAgent,
406
+
agent: Agent,
355
407
options: CreatePublicationOptions,
356
408
): Promise<string> {
357
409
let icon: BlobObject | undefined;
···
382
434
}
383
435
384
436
const response = await agent.com.atproto.repo.createRecord({
385
-
repo: agent.session!.did,
437
+
repo: agent.did!,
386
438
collection: "site.standard.publication",
387
439
record,
388
440
});
···
435
487
* Create a Bluesky post with external link embed
436
488
*/
437
489
export async function createBlueskyPost(
438
-
agent: AtpAgent,
490
+
agent: Agent,
439
491
options: CreateBlueskyPostOptions,
440
492
): Promise<StrongRef> {
441
493
const { title, description, canonicalUrl, coverImage, publishedAt } = options;
···
530
582
};
531
583
532
584
const response = await agent.com.atproto.repo.createRecord({
533
-
repo: agent.session!.did,
585
+
repo: agent.did!,
534
586
collection: "app.bsky.feed.post",
535
587
record,
536
588
});
···
545
597
* Add bskyPostRef to an existing document record
546
598
*/
547
599
export async function addBskyPostRefToDocument(
548
-
agent: AtpAgent,
600
+
agent: Agent,
549
601
documentAtUri: string,
550
602
bskyPostRef: StrongRef,
551
603
): Promise<void> {
+128
-26
packages/cli/src/lib/credentials.ts
+128
-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, we would need to check all OAuth sessions to find a matching handle,
99
+
// but handle matching isn't perfect without storing handles alongside sessions.
100
+
// For now, just return null if profile isn't a DID.
101
+
return null;
102
+
}
103
+
104
+
/**
55
105
* Load credentials for a specific identity or resolve which to use.
56
106
*
57
107
* Priority:
58
108
* 1. Full env vars (ATP_IDENTIFIER + ATP_APP_PASSWORD)
59
-
* 2. SEQUOIA_PROFILE env var - selects from stored credentials
109
+
* 2. SEQUOIA_PROFILE env var - selects from stored credentials (app-password or OAuth DID)
60
110
* 3. projectIdentity parameter (from sequoia.json)
61
-
* 4. If only one identity stored, use it
111
+
* 4. If only one identity stored (app-password or OAuth), use it
62
112
* 5. Return null (caller should prompt user)
63
113
*/
64
114
export async function loadCredentials(
···
71
121
72
122
if (envIdentifier && envPassword) {
73
123
return {
124
+
type: "app-password",
74
125
identifier: envIdentifier,
75
126
password: envPassword,
76
127
pdsUrl: envPdsUrl || "https://bsky.social",
···
78
129
}
79
130
80
131
const store = await loadCredentialsStore();
81
-
const identifiers = Object.keys(store);
82
-
83
-
if (identifiers.length === 0) {
84
-
return null;
85
-
}
132
+
const appPasswordIds = Object.keys(store);
133
+
const oauthDids = await listOAuthSessions();
86
134
87
135
// 2. SEQUOIA_PROFILE env var
88
136
const profileEnv = process.env.SEQUOIA_PROFILE;
89
-
if (profileEnv && store[profileEnv]) {
90
-
return store[profileEnv];
137
+
if (profileEnv) {
138
+
// Try app-password credentials first
139
+
if (store[profileEnv]) {
140
+
return normalizeCredentials(store[profileEnv]);
141
+
}
142
+
// Try OAuth session (profile could be a DID)
143
+
const oauth = await tryLoadOAuthCredentials(profileEnv);
144
+
if (oauth) {
145
+
return oauth;
146
+
}
91
147
}
92
148
93
149
// 3. Project-specific identity (from sequoia.json)
94
-
if (projectIdentity && store[projectIdentity]) {
95
-
return store[projectIdentity];
150
+
if (projectIdentity) {
151
+
if (store[projectIdentity]) {
152
+
return normalizeCredentials(store[projectIdentity]);
153
+
}
154
+
const oauth = await tryLoadOAuthCredentials(projectIdentity);
155
+
if (oauth) {
156
+
return oauth;
157
+
}
96
158
}
97
159
98
-
// 4. If only one identity, use it
99
-
if (identifiers.length === 1 && identifiers[0]) {
100
-
return store[identifiers[0]] ?? null;
160
+
// 4. If only one identity total, use it
161
+
const totalIdentities = appPasswordIds.length + oauthDids.length;
162
+
if (totalIdentities === 1) {
163
+
if (appPasswordIds.length === 1 && appPasswordIds[0]) {
164
+
return normalizeCredentials(store[appPasswordIds[0]]!);
165
+
}
166
+
if (oauthDids.length === 1 && oauthDids[0]) {
167
+
const session = await getOAuthSession(oauthDids[0]);
168
+
if (session) {
169
+
return {
170
+
type: "oauth",
171
+
did: oauthDids[0],
172
+
handle: oauthDids[0],
173
+
pdsUrl: "https://bsky.social",
174
+
};
175
+
}
176
+
}
101
177
}
102
178
103
-
// Multiple identities exist but none selected
179
+
// Multiple identities exist but none selected, or no identities
104
180
return null;
105
181
}
106
182
107
183
/**
108
-
* Get a specific identity by identifier
184
+
* Get a specific identity by identifier (app-password only)
109
185
*/
110
186
export async function getCredentials(
111
187
identifier: string,
112
-
): Promise<Credentials | null> {
188
+
): Promise<AppPasswordCredentials | null> {
113
189
const store = await loadCredentialsStore();
114
-
return store[identifier] || null;
190
+
const creds = store[identifier];
191
+
if (!creds) return null;
192
+
return normalizeCredentials(creds);
115
193
}
116
194
117
195
/**
118
-
* List all stored identities
196
+
* List all stored app-password identities
119
197
*/
120
198
export async function listCredentials(): Promise<string[]> {
121
199
const store = await loadCredentialsStore();
···
123
201
}
124
202
125
203
/**
126
-
* Save credentials for an identity (adds or updates)
204
+
* List all credentials (both app-password and OAuth)
127
205
*/
128
-
export async function saveCredentials(credentials: Credentials): Promise<void> {
206
+
export async function listAllCredentials(): Promise<
207
+
Array<{ id: string; type: "app-password" | "oauth" }>
208
+
> {
209
+
const store = await loadCredentialsStore();
210
+
const oauthDids = await listOAuthSessions();
211
+
212
+
const result: Array<{ id: string; type: "app-password" | "oauth" }> = [];
213
+
214
+
for (const id of Object.keys(store)) {
215
+
result.push({ id, type: "app-password" });
216
+
}
217
+
218
+
for (const did of oauthDids) {
219
+
result.push({ id: did, type: "oauth" });
220
+
}
221
+
222
+
return result;
223
+
}
224
+
225
+
/**
226
+
* Save app-password credentials for an identity (adds or updates)
227
+
*/
228
+
export async function saveCredentials(
229
+
credentials: AppPasswordCredentials,
230
+
): Promise<void> {
129
231
const store = await loadCredentialsStore();
130
232
store[credentials.identifier] = credentials;
131
233
await saveCredentialsStore(store);
+94
packages/cli/src/lib/oauth-client.ts
+94
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<T>(
22
+
key: string,
23
+
fn: () => T | PromiseLike<T>,
24
+
): Promise<T> {
25
+
// Wait for any existing lock on this key
26
+
while (locks.has(key)) {
27
+
await locks.get(key);
28
+
}
29
+
30
+
// Create our lock
31
+
let resolve: () => void;
32
+
const lockPromise = new Promise<void>((r) => {
33
+
resolve = r;
34
+
});
35
+
locks.set(key, lockPromise);
36
+
37
+
try {
38
+
return await fn();
39
+
} finally {
40
+
locks.delete(key);
41
+
resolve!();
42
+
}
43
+
}
44
+
45
+
/**
46
+
* Get or create the OAuth client singleton
47
+
*/
48
+
export async function getOAuthClient(): Promise<NodeOAuthClient> {
49
+
if (oauthClient) {
50
+
return oauthClient;
51
+
}
52
+
53
+
// Build client_id with required parameters
54
+
const clientIdParams = new URLSearchParams();
55
+
clientIdParams.append("redirect_uri", CALLBACK_URL);
56
+
clientIdParams.append("scope", OAUTH_SCOPE);
57
+
58
+
const clientOptions: NodeOAuthClientOptions = {
59
+
clientMetadata: {
60
+
client_id: `http://localhost?${clientIdParams.toString()}`,
61
+
client_name: "Sequoia CLI",
62
+
client_uri: "https://github.com/stevedylandev/sequoia",
63
+
redirect_uris: [CALLBACK_URL],
64
+
grant_types: ["authorization_code", "refresh_token"],
65
+
response_types: ["code"],
66
+
token_endpoint_auth_method: "none",
67
+
application_type: "web",
68
+
scope: OAUTH_SCOPE,
69
+
dpop_bound_access_tokens: false,
70
+
},
71
+
stateStore,
72
+
sessionStore,
73
+
// Configure identity resolution
74
+
plcDirectoryUrl: "https://plc.directory",
75
+
// Provide lock mechanism to prevent warning
76
+
requestLock,
77
+
};
78
+
79
+
oauthClient = new NodeOAuthClient(clientOptions);
80
+
81
+
return oauthClient;
82
+
}
83
+
84
+
export function getOAuthScope(): string {
85
+
return OAUTH_SCOPE;
86
+
}
87
+
88
+
export function getCallbackUrl(): string {
89
+
return CALLBACK_URL;
90
+
}
91
+
92
+
export function getCallbackPort(): number {
93
+
return CALLBACK_PORT;
94
+
}
+124
packages/cli/src/lib/oauth-store.ts
+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
+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 {
History
2 rounds
0 comments
stevedylan.dev
submitted
#1
5 commits
expand
collapse
feat: initial oauth implementation
chore: cleaned up types
chore: updated icon styles
chore: updated og image
chore: updated docs
1/1 success
expand
collapse
expand 0 comments
pull request successfully merged
stevedylan.dev
submitted
#0
4 commits
expand
collapse
feat: initial oauth implementation
chore: cleaned up types
chore: updated icon styles
chore: updated og image