tangled
alpha
login
or
join now
stevedylan.dev
/
sequoia
35
fork
atom
A CLI for publishing standard.site documents to ATProto
sequoia.pub
standard
site
lexicon
cli
publishing
35
fork
atom
overview
issues
5
pulls
1
pipelines
feat: initial oauth implementation
stevedylan.dev
1 month ago
cac03935
2dd2e1c3
+812
-44
10 changed files
expand all
collapse all
unified
split
bun.lock
packages
cli
package.json
src
commands
auth.ts
login.ts
index.ts
lib
atproto.ts
credentials.ts
oauth-client.ts
oauth-store.ts
types.ts
+67
-1
bun.lock
···
24
24
},
25
25
"packages/cli": {
26
26
"name": "sequoia-cli",
27
27
-
"version": "0.2.0",
27
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
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
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
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
55
+
56
56
+
"@atproto-labs/fetch": ["@atproto-labs/fetch@0.2.3", "", { "dependencies": { "@atproto-labs/pipe": "0.1.1" } }, "sha512-NZtbJOCbxKUFRFKMpamT38PUQMY0hX0p7TG5AEYOPhZKZEP7dHZ1K2s1aB8MdVH0qxmqX7nQleNrrvLf09Zfdw=="],
57
57
+
58
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
59
+
60
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
61
+
62
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
63
+
64
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
65
+
66
66
+
"@atproto-labs/pipe": ["@atproto-labs/pipe@0.1.1", "", {}, "sha512-hdNw2oUs2B6BN1lp+32pF7cp8EMKuIN5Qok2Vvv/aOpG/3tNSJ9YkvfI0k6Zd188LeDDYRUpYpxcoFIcGH/FNg=="],
67
67
+
68
68
+
"@atproto-labs/simple-store": ["@atproto-labs/simple-store@0.3.0", "", {}, "sha512-nOb6ONKBRJHRlukW1sVawUkBqReLlLx6hT35VS3imaNPwiXDxLnTK7lxw3Lrl9k5yugSBDQAkZAq3MPTEFSUBQ=="],
69
69
+
70
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
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
76
+
"@atproto/did": ["@atproto/did@0.3.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-raUPzUGegtW/6OxwCmM8bhZvuIMzxG5t9oWsth6Tp91Kb5fTnHV2h/KKNF1C82doeA4BdXCErTyg7ISwLbQkzA=="],
77
77
+
78
78
+
"@atproto/jwk": ["@atproto/jwk@0.6.0", "", { "dependencies": { "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-bDoJPvt7TrQVi/rBfBrSSpGykhtIriKxeYCYQTiPRKFfyRhbgpElF0wPXADjIswnbzZdOwbY63az4E/CFVT3Tw=="],
79
79
+
80
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
81
+
82
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
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
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
91
+
92
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
93
+
94
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
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
652
+
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
653
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
701
+
702
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
802
+
"default-browser": ["default-browser@5.4.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg=="],
803
803
+
804
804
+
"default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="],
805
805
+
806
806
+
"define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="],
807
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
968
+
"ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="],
969
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
976
+
"is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
977
977
+
930
978
"is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
931
979
980
980
+
"is-in-ssh": ["is-in-ssh@1.0.0", "", {}, "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw=="],
981
981
+
982
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
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
992
+
"is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="],
993
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
1001
+
1002
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
1225
+
1226
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
1270
+
"powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="],
1271
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
1345
+
1346
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
1440
+
"undici": ["undici@6.23.0", "", {}, "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g=="],
1441
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
1511
+
1512
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
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
37
-
"minimatch": "^10.1.1"
38
38
+
"minimatch": "^10.1.1",
39
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
161
+
type: "app-password",
161
162
pdsUrl,
162
163
identifier: identifier,
163
164
password: appPassword,
+296
packages/cli/src/commands/login.ts
···
1
1
+
import * as http from "node:http";
2
2
+
import { log, note, select, spinner, text } from "@clack/prompts";
3
3
+
import { command, flag, option, optional, string } from "cmd-ts";
4
4
+
import { resolveHandleToDid } from "../lib/atproto";
5
5
+
import {
6
6
+
getCallbackPort,
7
7
+
getCallbackUrl,
8
8
+
getOAuthClient,
9
9
+
getOAuthScope,
10
10
+
} from "../lib/oauth-client";
11
11
+
import {
12
12
+
deleteOAuthSession,
13
13
+
getOAuthStorePath,
14
14
+
listOAuthSessions,
15
15
+
} from "../lib/oauth-store";
16
16
+
import { exitOnCancel } from "../lib/prompts";
17
17
+
18
18
+
const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
19
19
+
20
20
+
export const loginCommand = command({
21
21
+
name: "login",
22
22
+
description: "Login with OAuth (browser-based authentication)",
23
23
+
args: {
24
24
+
logout: option({
25
25
+
long: "logout",
26
26
+
description: "Remove OAuth session for a specific DID",
27
27
+
type: optional(string),
28
28
+
}),
29
29
+
list: flag({
30
30
+
long: "list",
31
31
+
description: "List all stored OAuth sessions",
32
32
+
}),
33
33
+
},
34
34
+
handler: async ({ logout, list }) => {
35
35
+
// List sessions
36
36
+
if (list) {
37
37
+
const sessions = await listOAuthSessions();
38
38
+
if (sessions.length === 0) {
39
39
+
log.info("No OAuth sessions stored");
40
40
+
} else {
41
41
+
log.info("OAuth sessions:");
42
42
+
for (const did of sessions) {
43
43
+
console.log(` - ${did}`);
44
44
+
}
45
45
+
}
46
46
+
return;
47
47
+
}
48
48
+
49
49
+
// Logout
50
50
+
if (logout !== undefined) {
51
51
+
const did = logout || undefined;
52
52
+
53
53
+
if (!did) {
54
54
+
// No DID provided - show available and prompt
55
55
+
const sessions = await listOAuthSessions();
56
56
+
if (sessions.length === 0) {
57
57
+
log.info("No OAuth sessions found");
58
58
+
return;
59
59
+
}
60
60
+
if (sessions.length === 1) {
61
61
+
const deleted = await deleteOAuthSession(sessions[0]!);
62
62
+
if (deleted) {
63
63
+
log.success(`Removed OAuth session for ${sessions[0]}`);
64
64
+
}
65
65
+
return;
66
66
+
}
67
67
+
// Multiple sessions - prompt
68
68
+
const selected = exitOnCancel(
69
69
+
await select({
70
70
+
message: "Select session to remove:",
71
71
+
options: sessions.map((d) => ({ value: d, label: d })),
72
72
+
}),
73
73
+
);
74
74
+
const deleted = await deleteOAuthSession(selected);
75
75
+
if (deleted) {
76
76
+
log.success(`Removed OAuth session for ${selected}`);
77
77
+
}
78
78
+
return;
79
79
+
}
80
80
+
81
81
+
const deleted = await deleteOAuthSession(did);
82
82
+
if (deleted) {
83
83
+
log.success(`Removed OAuth session for ${did}`);
84
84
+
} else {
85
85
+
log.info(`No OAuth session found for ${did}`);
86
86
+
}
87
87
+
return;
88
88
+
}
89
89
+
90
90
+
// OAuth login flow
91
91
+
note(
92
92
+
"OAuth login will open your browser to authenticate.\n\n" +
93
93
+
"This is more secure than app passwords and tokens refresh automatically.",
94
94
+
"OAuth Login",
95
95
+
);
96
96
+
97
97
+
const handle = exitOnCancel(
98
98
+
await text({
99
99
+
message: "Handle or DID:",
100
100
+
placeholder: "yourhandle.bsky.social",
101
101
+
}),
102
102
+
);
103
103
+
104
104
+
if (!handle) {
105
105
+
log.error("Handle is required");
106
106
+
process.exit(1);
107
107
+
}
108
108
+
109
109
+
const s = spinner();
110
110
+
s.start("Resolving identity...");
111
111
+
112
112
+
let did: string;
113
113
+
try {
114
114
+
did = await resolveHandleToDid(handle);
115
115
+
s.stop(`Identity resolved`);
116
116
+
} catch (error) {
117
117
+
s.stop("Failed to resolve identity");
118
118
+
if (error instanceof Error) {
119
119
+
log.error(`Error: ${error.message}`);
120
120
+
} else {
121
121
+
log.error(`Error: ${error}`);
122
122
+
}
123
123
+
process.exit(1);
124
124
+
}
125
125
+
126
126
+
s.start("Initializing OAuth...");
127
127
+
128
128
+
try {
129
129
+
const client = await getOAuthClient();
130
130
+
131
131
+
// Generate authorization URL using the resolved DID
132
132
+
const authUrl = await client.authorize(did, {
133
133
+
scope: getOAuthScope(),
134
134
+
});
135
135
+
136
136
+
log.info(`Login URL: ${authUrl}`);
137
137
+
138
138
+
s.message("Opening browser...");
139
139
+
140
140
+
// Try to open browser
141
141
+
let browserOpened = true;
142
142
+
try {
143
143
+
const open = (await import("open")).default;
144
144
+
await open(authUrl.toString());
145
145
+
} catch {
146
146
+
browserOpened = false;
147
147
+
}
148
148
+
149
149
+
s.message("Waiting for authentication...");
150
150
+
151
151
+
// Show URL info
152
152
+
if (!browserOpened) {
153
153
+
s.stop("Could not open browser automatically");
154
154
+
log.warn("Please open the following URL in your browser:");
155
155
+
log.info(authUrl.toString());
156
156
+
s.start("Waiting for authentication...");
157
157
+
}
158
158
+
159
159
+
// Start HTTP server to receive callback
160
160
+
const result = await waitForCallback();
161
161
+
162
162
+
if (!result.success) {
163
163
+
s.stop("Authentication failed");
164
164
+
log.error(result.error || "OAuth callback failed");
165
165
+
process.exit(1);
166
166
+
}
167
167
+
168
168
+
s.message("Completing authentication...");
169
169
+
170
170
+
// Exchange code for tokens
171
171
+
const { session } = await client.callback(
172
172
+
new URLSearchParams(result.params!),
173
173
+
);
174
174
+
175
175
+
// Try to get the handle for display (use the original handle input as fallback)
176
176
+
let displayName = handle;
177
177
+
try {
178
178
+
// The session should have the DID, we can use the original handle they entered
179
179
+
// or we could fetch the profile to get the current handle
180
180
+
displayName = handle.startsWith("did:") ? session.did : handle;
181
181
+
} catch {
182
182
+
displayName = session.did;
183
183
+
}
184
184
+
185
185
+
s.stop(`Logged in as ${displayName}`);
186
186
+
187
187
+
log.success(`OAuth session saved to ${getOAuthStorePath()}`);
188
188
+
log.info("Your session will refresh automatically when needed.");
189
189
+
190
190
+
// Exit cleanly - the OAuth client may have background processes
191
191
+
process.exit(0);
192
192
+
} catch (error) {
193
193
+
s.stop("OAuth login failed");
194
194
+
if (error instanceof Error) {
195
195
+
log.error(`Error: ${error.message}`);
196
196
+
} else {
197
197
+
log.error(`Error: ${error}`);
198
198
+
}
199
199
+
process.exit(1);
200
200
+
}
201
201
+
},
202
202
+
});
203
203
+
204
204
+
interface CallbackResult {
205
205
+
success: boolean;
206
206
+
params?: Record<string, string>;
207
207
+
error?: string;
208
208
+
}
209
209
+
210
210
+
function waitForCallback(): Promise<CallbackResult> {
211
211
+
return new Promise((resolve) => {
212
212
+
const port = getCallbackPort();
213
213
+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
214
214
+
215
215
+
const server = http.createServer((req, res) => {
216
216
+
const url = new URL(req.url || "/", `http://127.0.0.1:${port}`);
217
217
+
218
218
+
if (url.pathname === "/oauth/callback") {
219
219
+
const params: Record<string, string> = {};
220
220
+
url.searchParams.forEach((value, key) => {
221
221
+
params[key] = value;
222
222
+
});
223
223
+
224
224
+
// Clear the timeout
225
225
+
if (timeoutId) clearTimeout(timeoutId);
226
226
+
227
227
+
// Check for error
228
228
+
if (params.error) {
229
229
+
res.writeHead(200, { "Content-Type": "text/html" });
230
230
+
res.end(`
231
231
+
<html>
232
232
+
<body style="font-family: system-ui; padding: 2rem; text-align: center;">
233
233
+
<h1>Authentication Failed</h1>
234
234
+
<p>${params.error_description || params.error}</p>
235
235
+
<p>You can close this window.</p>
236
236
+
</body>
237
237
+
</html>
238
238
+
`);
239
239
+
server.close(() => {
240
240
+
resolve({
241
241
+
success: false,
242
242
+
error: params.error_description || params.error,
243
243
+
});
244
244
+
});
245
245
+
return;
246
246
+
}
247
247
+
248
248
+
// Success
249
249
+
res.writeHead(200, { "Content-Type": "text/html" });
250
250
+
res.end(`
251
251
+
<html>
252
252
+
<body style="font-family: system-ui; padding: 2rem; text-align: center;">
253
253
+
<h1>Authentication Successful</h1>
254
254
+
<p>You can close this window and return to the terminal.</p>
255
255
+
</body>
256
256
+
</html>
257
257
+
`);
258
258
+
server.close(() => {
259
259
+
resolve({ success: true, params });
260
260
+
});
261
261
+
return;
262
262
+
}
263
263
+
264
264
+
// Not the callback path
265
265
+
res.writeHead(404);
266
266
+
res.end("Not found");
267
267
+
});
268
268
+
269
269
+
server.on("error", (err: NodeJS.ErrnoException) => {
270
270
+
if (timeoutId) clearTimeout(timeoutId);
271
271
+
if (err.code === "EADDRINUSE") {
272
272
+
resolve({
273
273
+
success: false,
274
274
+
error: `Port ${port} is already in use. Please close the application using that port and try again.`,
275
275
+
});
276
276
+
} else {
277
277
+
resolve({
278
278
+
success: false,
279
279
+
error: `Server error: ${err.message}`,
280
280
+
});
281
281
+
}
282
282
+
});
283
283
+
284
284
+
server.listen(port, "127.0.0.1");
285
285
+
286
286
+
// Timeout after 5 minutes
287
287
+
timeoutId = setTimeout(() => {
288
288
+
server.close(() => {
289
289
+
resolve({
290
290
+
success: false,
291
291
+
error: "Timeout waiting for OAuth callback. Please try again.",
292
292
+
});
293
293
+
});
294
294
+
}, CALLBACK_TIMEOUT_MS);
295
295
+
});
296
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
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
42
+
login: loginCommand,
41
43
publish: publishCommand,
42
44
sync: syncCommand,
43
45
},
+61
-15
packages/cli/src/lib/atproto.ts
···
1
1
-
import { AtpAgent } from "@atproto/api";
1
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
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
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
25
+
/**
26
26
+
* Resolve a handle to a DID
27
27
+
*/
28
28
+
export async function resolveHandleToDid(handle: string): Promise<string> {
29
29
+
if (handle.startsWith("did:")) {
30
30
+
return handle;
31
31
+
}
32
32
+
33
33
+
// Try to resolve handle via Bluesky API
34
34
+
const resolveUrl = `https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(handle)}`;
35
35
+
const resolveResponse = await fetch(resolveUrl);
36
36
+
if (!resolveResponse.ok) {
37
37
+
throw new Error("Could not resolve handle");
38
38
+
}
39
39
+
const resolveData = (await resolveResponse.json()) as { did: string };
40
40
+
return resolveData.did;
41
41
+
}
42
42
+
23
43
export async function resolveHandleToPDS(handle: string): Promise<string> {
24
44
// First, resolve the handle to a DID
25
25
-
let did: string;
26
26
-
27
27
-
if (handle.startsWith("did:")) {
28
28
-
did = handle;
29
29
-
} else {
30
30
-
// Try to resolve handle via Bluesky API
31
31
-
const resolveUrl = `https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(handle)}`;
32
32
-
const resolveResponse = await fetch(resolveUrl);
33
33
-
if (!resolveResponse.ok) {
34
34
-
throw new Error("Could not resolve handle");
35
35
-
}
36
36
-
const resolveData = (await resolveResponse.json()) as { did: string };
37
37
-
did = resolveData.did;
38
38
-
}
45
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
100
+
if (isOAuthCredentials(credentials)) {
101
101
+
// OAuth flow - restore session from stored tokens
102
102
+
const client = await getOAuthClient();
103
103
+
try {
104
104
+
const oauthSession = await client.restore(credentials.did);
105
105
+
// Wrap the OAuth session in an Agent which provides the atproto API
106
106
+
const agent = new Agent(oauthSession) as unknown as AtpAgent;
107
107
+
108
108
+
// The Agent class doesn't have session.did like AtpAgent does
109
109
+
// We need to set up a compatible session object for the rest of our code
110
110
+
agent.session = {
111
111
+
did: oauthSession.did,
112
112
+
handle: credentials.handle,
113
113
+
accessJwt: "",
114
114
+
refreshJwt: "",
115
115
+
active: true,
116
116
+
};
117
117
+
118
118
+
return agent;
119
119
+
} catch (error) {
120
120
+
if (error instanceof Error) {
121
121
+
// Check for common OAuth errors
122
122
+
if (
123
123
+
error.message.includes("expired") ||
124
124
+
error.message.includes("revoked")
125
125
+
) {
126
126
+
throw new Error(
127
127
+
`OAuth session expired or revoked. Please run 'sequoia login' to re-authenticate.`,
128
128
+
);
129
129
+
}
130
130
+
}
131
131
+
throw error;
132
132
+
}
133
133
+
}
134
134
+
135
135
+
// App password flow
136
136
+
if (!isAppPasswordCredentials(credentials)) {
137
137
+
throw new Error("Invalid credential type");
138
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
4
-
import type { Credentials } from "./types";
4
4
+
import { getOAuthSession, listOAuthSessions } from "./oauth-store";
5
5
+
import type {
6
6
+
AppPasswordCredentials,
7
7
+
Credentials,
8
8
+
LegacyCredentials,
9
9
+
OAuthCredentials,
10
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
9
-
// Stored credentials keyed by identifier
10
10
-
type CredentialsStore = Record<string, Credentials>;
15
15
+
// Stored credentials keyed by identifier (can be legacy or typed)
16
16
+
type CredentialsStore = Record<
17
17
+
string,
18
18
+
AppPasswordCredentials | LegacyCredentials
19
19
+
>;
11
20
12
21
async function fileExists(filePath: string): Promise<boolean> {
13
22
try {
···
19
28
}
20
29
21
30
/**
22
22
-
* Load all stored credentials
31
31
+
* Normalize credentials to have explicit type
23
32
*/
33
33
+
function normalizeCredentials(
34
34
+
creds: AppPasswordCredentials | LegacyCredentials,
35
35
+
): AppPasswordCredentials {
36
36
+
// If it already has type, return as-is
37
37
+
if ("type" in creds && creds.type === "app-password") {
38
38
+
return creds;
39
39
+
}
40
40
+
// Migrate legacy format
41
41
+
return {
42
42
+
type: "app-password",
43
43
+
pdsUrl: creds.pdsUrl,
44
44
+
identifier: creds.identifier,
45
45
+
password: creds.password,
46
46
+
};
47
47
+
}
48
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
35
-
const legacy = parsed as Credentials;
60
60
+
const legacy = parsed as LegacyCredentials;
36
61
return { [legacy.identifier]: legacy };
37
62
}
38
63
···
52
77
}
53
78
54
79
/**
80
80
+
* Try to load OAuth credentials for a given profile (DID or handle)
81
81
+
*/
82
82
+
async function tryLoadOAuthCredentials(
83
83
+
profile: string,
84
84
+
): Promise<OAuthCredentials | null> {
85
85
+
// If it looks like a DID, try to get the session directly
86
86
+
if (profile.startsWith("did:")) {
87
87
+
const session = await getOAuthSession(profile);
88
88
+
if (session) {
89
89
+
return {
90
90
+
type: "oauth",
91
91
+
did: profile,
92
92
+
handle: profile, // We don't have the handle stored, use DID
93
93
+
pdsUrl: "https://bsky.social", // Will be resolved from DID doc
94
94
+
};
95
95
+
}
96
96
+
}
97
97
+
98
98
+
// Otherwise, check all OAuth sessions to find a matching handle
99
99
+
// (This is a fallback - handle matching isn't perfect without storing handles)
100
100
+
const sessions = await listOAuthSessions();
101
101
+
for (const did of sessions) {
102
102
+
// Could enhance this by storing handle with session, but for now
103
103
+
// just return null if profile isn't a DID
104
104
+
}
105
105
+
106
106
+
return null;
107
107
+
}
108
108
+
109
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
59
-
* 2. SEQUOIA_PROFILE env var - selects from stored credentials
114
114
+
* 2. SEQUOIA_PROFILE env var - selects from stored credentials (app-password or OAuth DID)
60
115
* 3. projectIdentity parameter (from sequoia.json)
61
61
-
* 4. If only one identity stored, use it
116
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
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
81
-
const identifiers = Object.keys(store);
82
82
-
83
83
-
if (identifiers.length === 0) {
84
84
-
return null;
85
85
-
}
137
137
+
const appPasswordIds = Object.keys(store);
138
138
+
const oauthDids = await listOAuthSessions();
86
139
87
140
// 2. SEQUOIA_PROFILE env var
88
141
const profileEnv = process.env.SEQUOIA_PROFILE;
89
89
-
if (profileEnv && store[profileEnv]) {
90
90
-
return store[profileEnv];
142
142
+
if (profileEnv) {
143
143
+
// Try app-password credentials first
144
144
+
if (store[profileEnv]) {
145
145
+
return normalizeCredentials(store[profileEnv]);
146
146
+
}
147
147
+
// Try OAuth session (profile could be a DID)
148
148
+
const oauth = await tryLoadOAuthCredentials(profileEnv);
149
149
+
if (oauth) {
150
150
+
return oauth;
151
151
+
}
91
152
}
92
153
93
154
// 3. Project-specific identity (from sequoia.json)
94
94
-
if (projectIdentity && store[projectIdentity]) {
95
95
-
return store[projectIdentity];
155
155
+
if (projectIdentity) {
156
156
+
if (store[projectIdentity]) {
157
157
+
return normalizeCredentials(store[projectIdentity]);
158
158
+
}
159
159
+
const oauth = await tryLoadOAuthCredentials(projectIdentity);
160
160
+
if (oauth) {
161
161
+
return oauth;
162
162
+
}
96
163
}
97
164
98
98
-
// 4. If only one identity, use it
99
99
-
if (identifiers.length === 1 && identifiers[0]) {
100
100
-
return store[identifiers[0]] ?? null;
165
165
+
// 4. If only one identity total, use it
166
166
+
const totalIdentities = appPasswordIds.length + oauthDids.length;
167
167
+
if (totalIdentities === 1) {
168
168
+
if (appPasswordIds.length === 1 && appPasswordIds[0]) {
169
169
+
return normalizeCredentials(store[appPasswordIds[0]]!);
170
170
+
}
171
171
+
if (oauthDids.length === 1 && oauthDids[0]) {
172
172
+
const session = await getOAuthSession(oauthDids[0]);
173
173
+
if (session) {
174
174
+
return {
175
175
+
type: "oauth",
176
176
+
did: oauthDids[0],
177
177
+
handle: oauthDids[0],
178
178
+
pdsUrl: "https://bsky.social",
179
179
+
};
180
180
+
}
181
181
+
}
101
182
}
102
183
103
103
-
// Multiple identities exist but none selected
184
184
+
// Multiple identities exist but none selected, or no identities
104
185
return null;
105
186
}
106
187
107
188
/**
108
108
-
* Get a specific identity by identifier
189
189
+
* Get a specific identity by identifier (app-password only)
109
190
*/
110
191
export async function getCredentials(
111
192
identifier: string,
112
112
-
): Promise<Credentials | null> {
193
193
+
): Promise<AppPasswordCredentials | null> {
113
194
const store = await loadCredentialsStore();
114
114
-
return store[identifier] || null;
195
195
+
const creds = store[identifier];
196
196
+
if (!creds) return null;
197
197
+
return normalizeCredentials(creds);
115
198
}
116
199
117
200
/**
118
118
-
* List all stored identities
201
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
126
-
* Save credentials for an identity (adds or updates)
209
209
+
* List all credentials (both app-password and OAuth)
210
210
+
*/
211
211
+
export async function listAllCredentials(): Promise<
212
212
+
Array<{ id: string; type: "app-password" | "oauth" }>
213
213
+
> {
214
214
+
const store = await loadCredentialsStore();
215
215
+
const oauthDids = await listOAuthSessions();
216
216
+
217
217
+
const result: Array<{ id: string; type: "app-password" | "oauth" }> = [];
218
218
+
219
219
+
for (const id of Object.keys(store)) {
220
220
+
result.push({ id, type: "app-password" });
221
221
+
}
222
222
+
223
223
+
for (const did of oauthDids) {
224
224
+
result.push({ id: did, type: "oauth" });
225
225
+
}
226
226
+
227
227
+
return result;
228
228
+
}
229
229
+
230
230
+
/**
231
231
+
* Save app-password credentials for an identity (adds or updates)
127
232
*/
128
128
-
export async function saveCredentials(credentials: Credentials): Promise<void> {
233
233
+
export async function saveCredentials(
234
234
+
credentials: AppPasswordCredentials,
235
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
1
+
import {
2
2
+
NodeOAuthClient,
3
3
+
type NodeOAuthClientOptions,
4
4
+
} from "@atproto/oauth-client-node";
5
5
+
import { sessionStore, stateStore } from "./oauth-store";
6
6
+
7
7
+
const CALLBACK_PORT = 4000;
8
8
+
const CALLBACK_HOST = "127.0.0.1";
9
9
+
const CALLBACK_URL = `http://${CALLBACK_HOST}:${CALLBACK_PORT}/oauth/callback`;
10
10
+
11
11
+
// OAuth scope for Sequoia CLI - includes atproto base scope plus our collections
12
12
+
const OAUTH_SCOPE =
13
13
+
"atproto repo:site.standard.document repo:site.standard.publication repo:app.bsky.feed.post blob:*/*";
14
14
+
15
15
+
let oauthClient: NodeOAuthClient | null = null;
16
16
+
17
17
+
// Simple lock implementation for CLI (single process, no contention)
18
18
+
// This prevents the "No lock mechanism provided" warning
19
19
+
const locks = new Map<string, Promise<void>>();
20
20
+
21
21
+
async function requestLock(key: string, fn: () => Promise<void>): Promise<void> {
22
22
+
// Wait for any existing lock on this key
23
23
+
while (locks.has(key)) {
24
24
+
await locks.get(key);
25
25
+
}
26
26
+
27
27
+
// Create our lock
28
28
+
let resolve: () => void;
29
29
+
const lockPromise = new Promise<void>((r) => {
30
30
+
resolve = r;
31
31
+
});
32
32
+
locks.set(key, lockPromise);
33
33
+
34
34
+
try {
35
35
+
await fn();
36
36
+
} finally {
37
37
+
locks.delete(key);
38
38
+
resolve!();
39
39
+
}
40
40
+
}
41
41
+
42
42
+
/**
43
43
+
* Get or create the OAuth client singleton
44
44
+
*/
45
45
+
export async function getOAuthClient(): Promise<NodeOAuthClient> {
46
46
+
if (oauthClient) {
47
47
+
return oauthClient;
48
48
+
}
49
49
+
50
50
+
// Build client_id with required parameters
51
51
+
const clientIdParams = new URLSearchParams();
52
52
+
clientIdParams.append("redirect_uri", CALLBACK_URL);
53
53
+
clientIdParams.append("scope", OAUTH_SCOPE);
54
54
+
55
55
+
const clientOptions: NodeOAuthClientOptions = {
56
56
+
clientMetadata: {
57
57
+
client_id: `http://localhost?${clientIdParams.toString()}`,
58
58
+
client_name: "Sequoia CLI",
59
59
+
client_uri: "https://github.com/stevedylandev/sequoia",
60
60
+
redirect_uris: [CALLBACK_URL],
61
61
+
grant_types: ["authorization_code", "refresh_token"],
62
62
+
response_types: ["code"],
63
63
+
token_endpoint_auth_method: "none",
64
64
+
application_type: "web",
65
65
+
scope: OAUTH_SCOPE,
66
66
+
dpop_bound_access_tokens: false,
67
67
+
},
68
68
+
stateStore,
69
69
+
sessionStore,
70
70
+
// Configure identity resolution
71
71
+
plcDirectoryUrl: "https://plc.directory",
72
72
+
// Provide lock mechanism to prevent warning
73
73
+
requestLock,
74
74
+
};
75
75
+
76
76
+
oauthClient = new NodeOAuthClient(clientOptions);
77
77
+
78
78
+
return oauthClient;
79
79
+
}
80
80
+
81
81
+
export function getOAuthScope(): string {
82
82
+
return OAUTH_SCOPE;
83
83
+
}
84
84
+
85
85
+
export function getCallbackUrl(): string {
86
86
+
return CALLBACK_URL;
87
87
+
}
88
88
+
89
89
+
export function getCallbackPort(): number {
90
90
+
return CALLBACK_PORT;
91
91
+
}
+124
packages/cli/src/lib/oauth-store.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
4
+
import type {
5
5
+
NodeSavedSession,
6
6
+
NodeSavedSessionStore,
7
7
+
NodeSavedState,
8
8
+
NodeSavedStateStore,
9
9
+
} from "@atproto/oauth-client-node";
10
10
+
11
11
+
const CONFIG_DIR = path.join(os.homedir(), ".config", "sequoia");
12
12
+
const OAUTH_FILE = path.join(CONFIG_DIR, "oauth.json");
13
13
+
14
14
+
interface OAuthStore {
15
15
+
states: Record<string, NodeSavedState>;
16
16
+
sessions: Record<string, NodeSavedSession>;
17
17
+
}
18
18
+
19
19
+
async function fileExists(filePath: string): Promise<boolean> {
20
20
+
try {
21
21
+
await fs.access(filePath);
22
22
+
return true;
23
23
+
} catch {
24
24
+
return false;
25
25
+
}
26
26
+
}
27
27
+
28
28
+
async function loadOAuthStore(): Promise<OAuthStore> {
29
29
+
if (!(await fileExists(OAUTH_FILE))) {
30
30
+
return { states: {}, sessions: {} };
31
31
+
}
32
32
+
33
33
+
try {
34
34
+
const content = await fs.readFile(OAUTH_FILE, "utf-8");
35
35
+
return JSON.parse(content) as OAuthStore;
36
36
+
} catch {
37
37
+
return { states: {}, sessions: {} };
38
38
+
}
39
39
+
}
40
40
+
41
41
+
async function saveOAuthStore(store: OAuthStore): Promise<void> {
42
42
+
await fs.mkdir(CONFIG_DIR, { recursive: true });
43
43
+
await fs.writeFile(OAUTH_FILE, JSON.stringify(store, null, 2));
44
44
+
await fs.chmod(OAUTH_FILE, 0o600);
45
45
+
}
46
46
+
47
47
+
/**
48
48
+
* State store for PKCE flow (temporary, used during auth)
49
49
+
*/
50
50
+
export const stateStore: NodeSavedStateStore = {
51
51
+
async set(key: string, state: NodeSavedState): Promise<void> {
52
52
+
const store = await loadOAuthStore();
53
53
+
store.states[key] = state;
54
54
+
await saveOAuthStore(store);
55
55
+
},
56
56
+
57
57
+
async get(key: string): Promise<NodeSavedState | undefined> {
58
58
+
const store = await loadOAuthStore();
59
59
+
return store.states[key];
60
60
+
},
61
61
+
62
62
+
async del(key: string): Promise<void> {
63
63
+
const store = await loadOAuthStore();
64
64
+
delete store.states[key];
65
65
+
await saveOAuthStore(store);
66
66
+
},
67
67
+
};
68
68
+
69
69
+
/**
70
70
+
* Session store for OAuth tokens (persistent)
71
71
+
*/
72
72
+
export const sessionStore: NodeSavedSessionStore = {
73
73
+
async set(sub: string, session: NodeSavedSession): Promise<void> {
74
74
+
const store = await loadOAuthStore();
75
75
+
store.sessions[sub] = session;
76
76
+
await saveOAuthStore(store);
77
77
+
},
78
78
+
79
79
+
async get(sub: string): Promise<NodeSavedSession | undefined> {
80
80
+
const store = await loadOAuthStore();
81
81
+
return store.sessions[sub];
82
82
+
},
83
83
+
84
84
+
async del(sub: string): Promise<void> {
85
85
+
const store = await loadOAuthStore();
86
86
+
delete store.sessions[sub];
87
87
+
await saveOAuthStore(store);
88
88
+
},
89
89
+
};
90
90
+
91
91
+
/**
92
92
+
* List all stored OAuth session DIDs
93
93
+
*/
94
94
+
export async function listOAuthSessions(): Promise<string[]> {
95
95
+
const store = await loadOAuthStore();
96
96
+
return Object.keys(store.sessions);
97
97
+
}
98
98
+
99
99
+
/**
100
100
+
* Get an OAuth session by DID
101
101
+
*/
102
102
+
export async function getOAuthSession(
103
103
+
did: string,
104
104
+
): Promise<NodeSavedSession | undefined> {
105
105
+
const store = await loadOAuthStore();
106
106
+
return store.sessions[did];
107
107
+
}
108
108
+
109
109
+
/**
110
110
+
* Delete an OAuth session by DID
111
111
+
*/
112
112
+
export async function deleteOAuthSession(did: string): Promise<boolean> {
113
113
+
const store = await loadOAuthStore();
114
114
+
if (!store.sessions[did]) {
115
115
+
return false;
116
116
+
}
117
117
+
delete store.sessions[did];
118
118
+
await saveOAuthStore(store);
119
119
+
return true;
120
120
+
}
121
121
+
122
122
+
export function getOAuthStorePath(): string {
123
123
+
return OAUTH_FILE;
124
124
+
}
+34
-1
packages/cli/src/lib/types.ts
···
37
37
bluesky?: BlueskyConfig; // Optional Bluesky posting configuration
38
38
}
39
39
40
40
-
export interface Credentials {
40
40
+
// Legacy credentials format (for backward compatibility during migration)
41
41
+
export interface LegacyCredentials {
41
42
pdsUrl: string;
42
43
identifier: string;
43
44
password: string;
45
45
+
}
46
46
+
47
47
+
// App password credentials (explicit type)
48
48
+
export interface AppPasswordCredentials {
49
49
+
type: "app-password";
50
50
+
pdsUrl: string;
51
51
+
identifier: string;
52
52
+
password: string;
53
53
+
}
54
54
+
55
55
+
// OAuth credentials (references stored OAuth session)
56
56
+
export interface OAuthCredentials {
57
57
+
type: "oauth";
58
58
+
did: string;
59
59
+
handle: string;
60
60
+
pdsUrl: string;
61
61
+
}
62
62
+
63
63
+
// Union type for all credential types
64
64
+
export type Credentials = AppPasswordCredentials | OAuthCredentials;
65
65
+
66
66
+
// Helper to check credential type
67
67
+
export function isOAuthCredentials(
68
68
+
creds: Credentials,
69
69
+
): creds is OAuthCredentials {
70
70
+
return creds.type === "oauth";
71
71
+
}
72
72
+
73
73
+
export function isAppPasswordCredentials(
74
74
+
creds: Credentials,
75
75
+
): creds is AppPasswordCredentials {
76
76
+
return creds.type === "app-password";
44
77
}
45
78
46
79
export interface PostFrontmatter {