A CLI for publishing standard.site documents to ATProto sequoia.pub
standard site lexicon cli publishing

Add OAuth Login #8

merged opened by stevedylan.dev targeting main from feat/oauth
  • Adds a new login command 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
Labels

None yet.

assignee

None yet.

Participants 1
Referenced by
AT URI
at://did:plc:ia2zdnhjaokf5lazhxrmj6eu/sh.tangled.repo.pull/3mdv75qczng22
+873 -71
Diff #1
+67 -1
bun.lock
··· 24 }, 25 "packages/cli": { 26 "name": "sequoia-cli", 27 - "version": "0.2.0", 28 "bin": { 29 "sequoia": "dist/index.js", 30 }, 31 "dependencies": { 32 "@atproto/api": "^0.18.17", 33 "@clack/prompts": "^1.0.0", 34 "cmd-ts": "^0.14.3", 35 "glob": "^13.0.0", 36 "mime-types": "^2.1.35", 37 "minimatch": "^10.1.1", 38 }, 39 "devDependencies": { 40 "@biomejs/biome": "^2.3.13", ··· 49 "packages": { 50 "@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 52 "@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 54 "@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 56 "@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 58 "@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 60 "@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 62 "@atproto/syntax": ["@atproto/syntax@0.4.3", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-YoZUz40YAJr5nPwvCDWgodEOlt5IftZqPJvA0JDWjuZKD8yXddTwSzXSaKQAzGOpuM+/A3uXRtPzJJqlScc+iA=="], 63 64 "@atproto/xrpc": ["@atproto/xrpc@0.7.7", "", { "dependencies": { "@atproto/lexicon": "^0.6.0", "zod": "^3.23.8" } }, "sha512-K1ZyO/BU8JNtXX5dmPp7b5UrkLMMqpsIa/Lrj5D3Su+j1Xwq1m6QJ2XJ1AgjEjkI1v4Muzm7klianLE6XGxtmA=="], ··· 615 616 "bun-types": ["bun-types@1.3.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-qyschsA03Qz+gou+apt6HNl6HnI+sJJLL4wLDke4iugsE6584CMupOtTY1n+2YC9nGVrEKUlTs99jjRLKgWnjQ=="], 617 618 "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], 619 620 "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], ··· 662 "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], 663 664 "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], 665 666 "cose-base": ["cose-base@1.0.3", "", { "dependencies": { "layout-base": "^1.0.0" } }, "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg=="], 667 ··· 761 762 "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], 763 764 "delaunator": ["delaunator@5.0.1", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw=="], 765 766 "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], ··· 921 922 "internmap": ["internmap@1.0.1", "", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="], 923 924 "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], 925 926 "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 928 "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="], 929 930 "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], 931 932 "is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="], 933 934 "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], ··· 937 938 "is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="], 939 940 "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], 941 942 "iso-datestring-validator": ["iso-datestring-validator@2.2.2", "", {}, "sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA=="], ··· 944 "javascript-stringify": ["javascript-stringify@2.1.0", "", {}, "sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg=="], 945 946 "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], 947 948 "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], 949 ··· 1166 "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], 1167 1168 "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=="], 1169 1170 "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 ··· 1209 1210 "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], 1211 1212 "property-information": ["property-information@6.5.0", "", {}, "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig=="], 1213 1214 "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 "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 1284 "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=="], 1285 1286 "rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="], 1287 ··· 1375 1376 "uint8arrays": ["uint8arrays@3.0.0", "", { "dependencies": { "multiformats": "^9.4.2" } }, "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA=="], 1377 1378 "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], 1379 1380 "unicode-segmenter": ["unicode-segmenter@0.14.5", "", {}, "sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g=="], ··· 1444 "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], 1445 1446 "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], 1447 1448 "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], 1449
··· 24 }, 25 "packages/cli": { 26 "name": "sequoia-cli", 27 + "version": "0.2.1", 28 "bin": { 29 "sequoia": "dist/index.js", 30 }, 31 "dependencies": { 32 "@atproto/api": "^0.18.17", 33 + "@atproto/oauth-client-node": "^0.3.16", 34 "@clack/prompts": "^1.0.0", 35 "cmd-ts": "^0.14.3", 36 "glob": "^13.0.0", 37 "mime-types": "^2.1.35", 38 "minimatch": "^10.1.1", 39 + "open": "^11.0.0", 40 }, 41 "devDependencies": { 42 "@biomejs/biome": "^2.3.13", ··· 51 "packages": { 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=="], 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 + 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=="], 73 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=="], 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 + 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=="], 85 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=="], 87 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=="], 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 + 96 "@atproto/syntax": ["@atproto/syntax@0.4.3", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-YoZUz40YAJr5nPwvCDWgodEOlt5IftZqPJvA0JDWjuZKD8yXddTwSzXSaKQAzGOpuM+/A3uXRtPzJJqlScc+iA=="], 97 98 "@atproto/xrpc": ["@atproto/xrpc@0.7.7", "", { "dependencies": { "@atproto/lexicon": "^0.6.0", "zod": "^3.23.8" } }, "sha512-K1ZyO/BU8JNtXX5dmPp7b5UrkLMMqpsIa/Lrj5D3Su+j1Xwq1m6QJ2XJ1AgjEjkI1v4Muzm7klianLE6XGxtmA=="], ··· 649 650 "bun-types": ["bun-types@1.3.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-qyschsA03Qz+gou+apt6HNl6HnI+sJJLL4wLDke4iugsE6584CMupOtTY1n+2YC9nGVrEKUlTs99jjRLKgWnjQ=="], 651 652 + "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], 653 + 654 "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], 655 656 "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], ··· 698 "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], 699 700 "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], 701 + 702 + "core-js": ["core-js@3.48.0", "", {}, "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ=="], 703 704 "cose-base": ["cose-base@1.0.3", "", { "dependencies": { "layout-base": "^1.0.0" } }, "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg=="], 705 ··· 799 800 "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], 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 + 808 "delaunator": ["delaunator@5.0.1", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw=="], 809 810 "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], ··· 965 966 "internmap": ["internmap@1.0.1", "", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="], 967 968 + "ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="], 969 + 970 "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], 971 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=="], 973 974 "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="], 975 976 + "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], 977 + 978 "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], 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 + 984 "is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="], 985 986 "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], ··· 989 990 "is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="], 991 992 + "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], 993 + 994 "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], 995 996 "iso-datestring-validator": ["iso-datestring-validator@2.2.2", "", {}, "sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA=="], ··· 998 "javascript-stringify": ["javascript-stringify@2.1.0", "", {}, "sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg=="], 999 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=="], 1003 1004 "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], 1005 ··· 1222 "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], 1223 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=="], 1227 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=="], 1229 ··· 1267 1268 "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], 1269 1270 + "powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="], 1271 + 1272 "property-information": ["property-information@6.5.0", "", {}, "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig=="], 1273 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=="], ··· 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=="], 1343 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=="], 1347 1348 "rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="], 1349 ··· 1437 1438 "uint8arrays": ["uint8arrays@3.0.0", "", { "dependencies": { "multiformats": "^9.4.2" } }, "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA=="], 1439 1440 + "undici": ["undici@6.23.0", "", {}, "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g=="], 1441 + 1442 "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], 1443 1444 "unicode-segmenter": ["unicode-segmenter@0.14.5", "", {}, "sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g=="], ··· 1508 "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], 1509 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=="], 1513 1514 "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], 1515
+19 -1
docs/docs/pages/cli-reference.mdx
··· 1 # CLI Reference 2 3 ## `auth` 4 5 ```bash [Terminal] 6 sequoia auth 7 - > Authenticate with your ATProto PDS 8 9 OPTIONS: 10 --logout <str> - Remove credentials for a specific identity (or all if only one exists) [optional] ··· 13 --list - List all stored identities [optional] 14 --help, -h - show help [optional] 15 ``` 16 17 ## `init` 18
··· 1 # CLI Reference 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 + 19 ## `auth` 20 21 ```bash [Terminal] 22 sequoia auth 23 + > Authenticate with your ATProto PDS using an app password 24 25 OPTIONS: 26 --logout <str> - Remove credentials for a specific identity (or all if only one exists) [optional] ··· 29 --list - List all stored identities [optional] 30 --help, -h - show help [optional] 31 ``` 32 + 33 + Use this as an alternative to `login` when OAuth isn't available or for CI environments. 34 35 ## `init` 36
+9 -7
docs/docs/pages/quickstart.mdx
··· 31 sequoia 32 ``` 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. 37 38 - :::tip 39 - You can create an app password [here](https://bsky.app/settings/app-passwords) 40 - ::: 41 42 ```bash [Terminal] 43 - sequoia auth 44 ``` 45 46 ### Initialize 47
··· 31 sequoia 32 ``` 33 34 + ### Login 35 36 + In order for Sequoia to publish or update records on your PDS, you need to authenticate with your ATProto account. 37 38 ```bash [Terminal] 39 + sequoia login 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 + ::: 47 48 ### Initialize 49
docs/docs/public/icon-dark.png

This is a binary file and will not be displayed.

docs/docs/public/og.png

This is a binary file and will not be displayed.

+3 -1
packages/cli/package.json
··· 30 }, 31 "dependencies": { 32 "@atproto/api": "^0.18.17", 33 "@clack/prompts": "^1.0.0", 34 "cmd-ts": "^0.14.3", 35 "glob": "^13.0.0", 36 "mime-types": "^2.1.35", 37 - "minimatch": "^10.1.1" 38 } 39 }
··· 30 }, 31 "dependencies": { 32 "@atproto/api": "^0.18.17", 33 + "@atproto/oauth-client-node": "^0.3.16", 34 "@clack/prompts": "^1.0.0", 35 "cmd-ts": "^0.14.3", 36 "glob": "^13.0.0", 37 "mime-types": "^2.1.35", 38 + "minimatch": "^10.1.1", 39 + "open": "^11.0.0" 40 } 41 }
+1
packages/cli/src/commands/auth.ts
··· 158 159 // Save credentials 160 await saveCredentials({ 161 pdsUrl, 162 identifier: identifier, 163 password: appPassword,
··· 158 159 // Save credentials 160 await saveCredentials({ 161 + type: "app-password", 162 pdsUrl, 163 identifier: identifier, 164 password: appPassword,
+4 -1
packages/cli/src/commands/init.ts
··· 287 defaultValue: "7", 288 placeholder: "7", 289 validate: (value) => { 290 - const num = parseInt(value, 10); 291 if (Number.isNaN(num) || num < 1) { 292 return "Please enter a positive number"; 293 }
··· 287 defaultValue: "7", 288 placeholder: "7", 289 validate: (value) => { 290 + if (!value) { 291 + return "Please enter a number"; 292 + } 293 + const num = Number.parseInt(value, 10); 294 if (Number.isNaN(num) || num < 1) { 295 return "Please enter a positive number"; 296 }
+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
··· 209 let agent: Awaited<ReturnType<typeof createAgent>> | undefined; 210 try { 211 agent = await createAgent(credentials); 212 - s.stop(`Logged in as ${agent.session?.handle}`); 213 } catch (error) { 214 s.stop("Failed to login"); 215 log.error(`Failed to login: ${error}`);
··· 209 let agent: Awaited<ReturnType<typeof createAgent>> | undefined; 210 try { 211 agent = await createAgent(credentials); 212 + s.stop(`Logged in as ${agent.did}`); 213 } catch (error) { 214 s.stop("Failed to login"); 215 log.error(`Failed to login: ${error}`);
+1 -1
packages/cli/src/commands/sync.ts
··· 76 let agent: Awaited<ReturnType<typeof createAgent>> | undefined; 77 try { 78 agent = await createAgent(credentials); 79 - s.stop(`Logged in as ${agent.session?.handle}`); 80 } catch (error) { 81 s.stop("Failed to login"); 82 log.error(`Failed to login: ${error}`);
··· 76 let agent: Awaited<ReturnType<typeof createAgent>> | undefined; 77 try { 78 agent = await createAgent(credentials); 79 + s.stop(`Logged in as ${agent.did}`); 80 } catch (error) { 81 s.stop("Failed to login"); 82 log.error(`Failed to login: ${error}`);
+2
packages/cli/src/index.ts
··· 4 import { authCommand } from "./commands/auth"; 5 import { initCommand } from "./commands/init"; 6 import { injectCommand } from "./commands/inject"; 7 import { publishCommand } from "./commands/publish"; 8 import { syncCommand } from "./commands/sync"; 9 ··· 38 auth: authCommand, 39 init: initCommand, 40 inject: injectCommand, 41 publish: publishCommand, 42 sync: syncCommand, 43 },
··· 4 import { authCommand } from "./commands/auth"; 5 import { initCommand } from "./commands/init"; 6 import { injectCommand } from "./commands/inject"; 7 + import { loginCommand } from "./commands/login"; 8 import { publishCommand } from "./commands/publish"; 9 import { syncCommand } from "./commands/sync"; 10 ··· 39 auth: authCommand, 40 init: initCommand, 41 inject: injectCommand, 42 + login: loginCommand, 43 publish: publishCommand, 44 sync: syncCommand, 45 },
+83 -31
packages/cli/src/lib/atproto.ts
··· 1 - import { AtpAgent } from "@atproto/api"; 2 import * as mimeTypes from "mime-types"; 3 import * as fs from "node:fs/promises"; 4 import * as path from "node:path"; 5 import { stripMarkdownForText } from "./markdown"; 6 import type { 7 BlobObject, 8 BlogPost, ··· 10 PublisherConfig, 11 StrongRef, 12 } from "./types"; 13 14 async function fileExists(filePath: string): Promise<boolean> { 15 try { ··· 20 } 21 } 22 23 export async function resolveHandleToPDS(handle: string): Promise<string> { 24 // 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 - } 39 40 // Now resolve the DID to get the PDS URL from the DID document 41 let pdsUrl: string | undefined; ··· 89 showInDiscover?: boolean; 90 } 91 92 - export async function createAgent(credentials: Credentials): Promise<AtpAgent> { 93 const agent = new AtpAgent({ service: credentials.pdsUrl }); 94 95 await agent.login({ ··· 101 } 102 103 export async function uploadImage( 104 - agent: AtpAgent, 105 imagePath: string, 106 ): Promise<BlobObject | undefined> { 107 if (!(await fileExists(imagePath))) { ··· 170 } 171 172 export async function createDocument( 173 - agent: AtpAgent, 174 post: BlogPost, 175 config: PublisherConfig, 176 coverImage?: BlobObject, ··· 213 } 214 215 const response = await agent.com.atproto.repo.createRecord({ 216 - repo: agent.session!.did, 217 collection: "site.standard.document", 218 record, 219 }); ··· 222 } 223 224 export async function updateDocument( 225 - agent: AtpAgent, 226 post: BlogPost, 227 atUri: string, 228 config: PublisherConfig, ··· 275 } 276 277 await agent.com.atproto.repo.putRecord({ 278 - repo: agent.session!.did, 279 collection: collection!, 280 rkey: rkey!, 281 record, ··· 315 } 316 317 export async function listDocuments( 318 - agent: AtpAgent, 319 publicationUri?: string, 320 ): Promise<ListDocumentsResult[]> { 321 const documents: ListDocumentsResult[] = []; ··· 323 324 do { 325 const response = await agent.com.atproto.repo.listRecords({ 326 - repo: agent.session!.did, 327 collection: "site.standard.document", 328 limit: 100, 329 cursor, 330 }); 331 332 for (const record of response.data.records) { 333 - const value = record.value as unknown as DocumentRecord; 334 335 // If publicationUri is specified, only include documents from that publication 336 - if (publicationUri && value.site !== publicationUri) { 337 continue; 338 } 339 340 documents.push({ 341 uri: record.uri, 342 cid: record.cid, 343 - value, 344 }); 345 } 346 ··· 351 } 352 353 export async function createPublication( 354 - agent: AtpAgent, 355 options: CreatePublicationOptions, 356 ): Promise<string> { 357 let icon: BlobObject | undefined; ··· 382 } 383 384 const response = await agent.com.atproto.repo.createRecord({ 385 - repo: agent.session!.did, 386 collection: "site.standard.publication", 387 record, 388 }); ··· 435 * Create a Bluesky post with external link embed 436 */ 437 export async function createBlueskyPost( 438 - agent: AtpAgent, 439 options: CreateBlueskyPostOptions, 440 ): Promise<StrongRef> { 441 const { title, description, canonicalUrl, coverImage, publishedAt } = options; ··· 530 }; 531 532 const response = await agent.com.atproto.repo.createRecord({ 533 - repo: agent.session!.did, 534 collection: "app.bsky.feed.post", 535 record, 536 }); ··· 545 * Add bskyPostRef to an existing document record 546 */ 547 export async function addBskyPostRefToDocument( 548 - agent: AtpAgent, 549 documentAtUri: string, 550 bskyPostRef: StrongRef, 551 ): Promise<void> {
··· 1 + import { Agent, AtpAgent } from "@atproto/api"; 2 import * as mimeTypes from "mime-types"; 3 import * as fs from "node:fs/promises"; 4 import * as path from "node:path"; 5 import { stripMarkdownForText } from "./markdown"; 6 + import { getOAuthClient } from "./oauth-client"; 7 import type { 8 BlobObject, 9 BlogPost, ··· 11 PublisherConfig, 12 StrongRef, 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 + } 31 32 async function fileExists(filePath: string): Promise<boolean> { 33 try { ··· 38 } 39 } 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 + 59 export async function resolveHandleToPDS(handle: string): Promise<string> { 60 // First, resolve the handle to a DID 61 + const did = await resolveHandleToDid(handle); 62 63 // Now resolve the DID to get the PDS URL from the DID document 64 let pdsUrl: string | undefined; ··· 112 showInDiscover?: boolean; 113 } 114 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 + } 143 const agent = new AtpAgent({ service: credentials.pdsUrl }); 144 145 await agent.login({ ··· 151 } 152 153 export async function uploadImage( 154 + agent: Agent, 155 imagePath: string, 156 ): Promise<BlobObject | undefined> { 157 if (!(await fileExists(imagePath))) { ··· 220 } 221 222 export async function createDocument( 223 + agent: Agent, 224 post: BlogPost, 225 config: PublisherConfig, 226 coverImage?: BlobObject, ··· 263 } 264 265 const response = await agent.com.atproto.repo.createRecord({ 266 + repo: agent.did!, 267 collection: "site.standard.document", 268 record, 269 }); ··· 272 } 273 274 export async function updateDocument( 275 + agent: Agent, 276 post: BlogPost, 277 atUri: string, 278 config: PublisherConfig, ··· 325 } 326 327 await agent.com.atproto.repo.putRecord({ 328 + repo: agent.did!, 329 collection: collection!, 330 rkey: rkey!, 331 record, ··· 365 } 366 367 export async function listDocuments( 368 + agent: Agent, 369 publicationUri?: string, 370 ): Promise<ListDocumentsResult[]> { 371 const documents: ListDocumentsResult[] = []; ··· 373 374 do { 375 const response = await agent.com.atproto.repo.listRecords({ 376 + repo: agent.did!, 377 collection: "site.standard.document", 378 limit: 100, 379 cursor, 380 }); 381 382 for (const record of response.data.records) { 383 + if (!isDocumentRecord(record.value)) { 384 + continue; 385 + } 386 387 // If publicationUri is specified, only include documents from that publication 388 + if (publicationUri && record.value.site !== publicationUri) { 389 continue; 390 } 391 392 documents.push({ 393 uri: record.uri, 394 cid: record.cid, 395 + value: record.value, 396 }); 397 } 398 ··· 403 } 404 405 export async function createPublication( 406 + agent: Agent, 407 options: CreatePublicationOptions, 408 ): Promise<string> { 409 let icon: BlobObject | undefined; ··· 434 } 435 436 const response = await agent.com.atproto.repo.createRecord({ 437 + repo: agent.did!, 438 collection: "site.standard.publication", 439 record, 440 }); ··· 487 * Create a Bluesky post with external link embed 488 */ 489 export async function createBlueskyPost( 490 + agent: Agent, 491 options: CreateBlueskyPostOptions, 492 ): Promise<StrongRef> { 493 const { title, description, canonicalUrl, coverImage, publishedAt } = options; ··· 582 }; 583 584 const response = await agent.com.atproto.repo.createRecord({ 585 + repo: agent.did!, 586 collection: "app.bsky.feed.post", 587 record, 588 }); ··· 597 * Add bskyPostRef to an existing document record 598 */ 599 export async function addBskyPostRefToDocument( 600 + agent: Agent, 601 documentAtUri: string, 602 bskyPostRef: StrongRef, 603 ): Promise<void> {
+128 -26
packages/cli/src/lib/credentials.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 { Credentials } from "./types"; 5 6 const CONFIG_DIR = path.join(os.homedir(), ".config", "sequoia"); 7 const CREDENTIALS_FILE = path.join(CONFIG_DIR, "credentials.json"); 8 9 - // Stored credentials keyed by identifier 10 - type CredentialsStore = Record<string, Credentials>; 11 12 async function fileExists(filePath: string): Promise<boolean> { 13 try { ··· 19 } 20 21 /** 22 - * Load all stored credentials 23 */ 24 async function loadCredentialsStore(): Promise<CredentialsStore> { 25 if (!(await fileExists(CREDENTIALS_FILE))) { 26 return {}; ··· 32 33 // Handle legacy single-credential format (migrate on read) 34 if (parsed.identifier && parsed.password) { 35 - const legacy = parsed as Credentials; 36 return { [legacy.identifier]: legacy }; 37 } 38 ··· 52 } 53 54 /** 55 * Load credentials for a specific identity or resolve which to use. 56 * 57 * Priority: 58 * 1. Full env vars (ATP_IDENTIFIER + ATP_APP_PASSWORD) 59 - * 2. SEQUOIA_PROFILE env var - selects from stored credentials 60 * 3. projectIdentity parameter (from sequoia.json) 61 - * 4. If only one identity stored, use it 62 * 5. Return null (caller should prompt user) 63 */ 64 export async function loadCredentials( ··· 71 72 if (envIdentifier && envPassword) { 73 return { 74 identifier: envIdentifier, 75 password: envPassword, 76 pdsUrl: envPdsUrl || "https://bsky.social", ··· 78 } 79 80 const store = await loadCredentialsStore(); 81 - const identifiers = Object.keys(store); 82 - 83 - if (identifiers.length === 0) { 84 - return null; 85 - } 86 87 // 2. SEQUOIA_PROFILE env var 88 const profileEnv = process.env.SEQUOIA_PROFILE; 89 - if (profileEnv && store[profileEnv]) { 90 - return store[profileEnv]; 91 } 92 93 // 3. Project-specific identity (from sequoia.json) 94 - if (projectIdentity && store[projectIdentity]) { 95 - return store[projectIdentity]; 96 } 97 98 - // 4. If only one identity, use it 99 - if (identifiers.length === 1 && identifiers[0]) { 100 - return store[identifiers[0]] ?? null; 101 } 102 103 - // Multiple identities exist but none selected 104 return null; 105 } 106 107 /** 108 - * Get a specific identity by identifier 109 */ 110 export async function getCredentials( 111 identifier: string, 112 - ): Promise<Credentials | null> { 113 const store = await loadCredentialsStore(); 114 - return store[identifier] || null; 115 } 116 117 /** 118 - * List all stored identities 119 */ 120 export async function listCredentials(): Promise<string[]> { 121 const store = await loadCredentialsStore(); ··· 123 } 124 125 /** 126 - * Save credentials for an identity (adds or updates) 127 */ 128 - export async function saveCredentials(credentials: Credentials): Promise<void> { 129 const store = await loadCredentialsStore(); 130 store[credentials.identifier] = credentials; 131 await saveCredentialsStore(store);
··· 1 import * as fs from "node:fs/promises"; 2 import * as os from "node:os"; 3 import * as path from "node:path"; 4 + import { getOAuthSession, listOAuthSessions } from "./oauth-store"; 5 + import type { 6 + AppPasswordCredentials, 7 + Credentials, 8 + LegacyCredentials, 9 + OAuthCredentials, 10 + } from "./types"; 11 12 const CONFIG_DIR = path.join(os.homedir(), ".config", "sequoia"); 13 const CREDENTIALS_FILE = path.join(CONFIG_DIR, "credentials.json"); 14 15 + // Stored credentials keyed by identifier (can be legacy or typed) 16 + type CredentialsStore = Record< 17 + string, 18 + AppPasswordCredentials | LegacyCredentials 19 + >; 20 21 async function fileExists(filePath: string): Promise<boolean> { 22 try { ··· 28 } 29 30 /** 31 + * Normalize credentials to have explicit type 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 + 49 async function loadCredentialsStore(): Promise<CredentialsStore> { 50 if (!(await fileExists(CREDENTIALS_FILE))) { 51 return {}; ··· 57 58 // Handle legacy single-credential format (migrate on read) 59 if (parsed.identifier && parsed.password) { 60 + const legacy = parsed as LegacyCredentials; 61 return { [legacy.identifier]: legacy }; 62 } 63 ··· 77 } 78 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 + /** 105 * Load credentials for a specific identity or resolve which to use. 106 * 107 * Priority: 108 * 1. Full env vars (ATP_IDENTIFIER + ATP_APP_PASSWORD) 109 + * 2. SEQUOIA_PROFILE env var - selects from stored credentials (app-password or OAuth DID) 110 * 3. projectIdentity parameter (from sequoia.json) 111 + * 4. If only one identity stored (app-password or OAuth), use it 112 * 5. Return null (caller should prompt user) 113 */ 114 export async function loadCredentials( ··· 121 122 if (envIdentifier && envPassword) { 123 return { 124 + type: "app-password", 125 identifier: envIdentifier, 126 password: envPassword, 127 pdsUrl: envPdsUrl || "https://bsky.social", ··· 129 } 130 131 const store = await loadCredentialsStore(); 132 + const appPasswordIds = Object.keys(store); 133 + const oauthDids = await listOAuthSessions(); 134 135 // 2. SEQUOIA_PROFILE env var 136 const profileEnv = process.env.SEQUOIA_PROFILE; 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 + } 147 } 148 149 // 3. Project-specific identity (from sequoia.json) 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 + } 158 } 159 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 + } 177 } 178 179 + // Multiple identities exist but none selected, or no identities 180 return null; 181 } 182 183 /** 184 + * Get a specific identity by identifier (app-password only) 185 */ 186 export async function getCredentials( 187 identifier: string, 188 + ): Promise<AppPasswordCredentials | null> { 189 const store = await loadCredentialsStore(); 190 + const creds = store[identifier]; 191 + if (!creds) return null; 192 + return normalizeCredentials(creds); 193 } 194 195 /** 196 + * List all stored app-password identities 197 */ 198 export async function listCredentials(): Promise<string[]> { 199 const store = await loadCredentialsStore(); ··· 201 } 202 203 /** 204 + * List all credentials (both app-password and OAuth) 205 */ 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> { 231 const store = await loadCredentialsStore(); 232 store[credentials.identifier] = credentials; 233 await saveCredentialsStore(store);
+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
···
··· 1 + import * as fs from "node:fs/promises"; 2 + import * as os from "node:os"; 3 + import * as path from "node:path"; 4 + import type { 5 + NodeSavedSession, 6 + NodeSavedSessionStore, 7 + NodeSavedState, 8 + NodeSavedStateStore, 9 + } from "@atproto/oauth-client-node"; 10 + 11 + const CONFIG_DIR = path.join(os.homedir(), ".config", "sequoia"); 12 + const OAUTH_FILE = path.join(CONFIG_DIR, "oauth.json"); 13 + 14 + interface OAuthStore { 15 + states: Record<string, NodeSavedState>; 16 + sessions: Record<string, NodeSavedSession>; 17 + } 18 + 19 + async function fileExists(filePath: string): Promise<boolean> { 20 + try { 21 + await fs.access(filePath); 22 + return true; 23 + } catch { 24 + return false; 25 + } 26 + } 27 + 28 + async function loadOAuthStore(): Promise<OAuthStore> { 29 + if (!(await fileExists(OAUTH_FILE))) { 30 + return { states: {}, sessions: {} }; 31 + } 32 + 33 + try { 34 + const content = await fs.readFile(OAUTH_FILE, "utf-8"); 35 + return JSON.parse(content) as OAuthStore; 36 + } catch { 37 + return { states: {}, sessions: {} }; 38 + } 39 + } 40 + 41 + async function saveOAuthStore(store: OAuthStore): Promise<void> { 42 + await fs.mkdir(CONFIG_DIR, { recursive: true }); 43 + await fs.writeFile(OAUTH_FILE, JSON.stringify(store, null, 2)); 44 + await fs.chmod(OAUTH_FILE, 0o600); 45 + } 46 + 47 + /** 48 + * State store for PKCE flow (temporary, used during auth) 49 + */ 50 + export const stateStore: NodeSavedStateStore = { 51 + async set(key: string, state: NodeSavedState): Promise<void> { 52 + const store = await loadOAuthStore(); 53 + store.states[key] = state; 54 + await saveOAuthStore(store); 55 + }, 56 + 57 + async get(key: string): Promise<NodeSavedState | undefined> { 58 + const store = await loadOAuthStore(); 59 + return store.states[key]; 60 + }, 61 + 62 + async del(key: string): Promise<void> { 63 + const store = await loadOAuthStore(); 64 + delete store.states[key]; 65 + await saveOAuthStore(store); 66 + }, 67 + }; 68 + 69 + /** 70 + * Session store for OAuth tokens (persistent) 71 + */ 72 + export const sessionStore: NodeSavedSessionStore = { 73 + async set(sub: string, session: NodeSavedSession): Promise<void> { 74 + const store = await loadOAuthStore(); 75 + store.sessions[sub] = session; 76 + await saveOAuthStore(store); 77 + }, 78 + 79 + async get(sub: string): Promise<NodeSavedSession | undefined> { 80 + const store = await loadOAuthStore(); 81 + return store.sessions[sub]; 82 + }, 83 + 84 + async del(sub: string): Promise<void> { 85 + const store = await loadOAuthStore(); 86 + delete store.sessions[sub]; 87 + await saveOAuthStore(store); 88 + }, 89 + }; 90 + 91 + /** 92 + * List all stored OAuth session DIDs 93 + */ 94 + export async function listOAuthSessions(): Promise<string[]> { 95 + const store = await loadOAuthStore(); 96 + return Object.keys(store.sessions); 97 + } 98 + 99 + /** 100 + * Get an OAuth session by DID 101 + */ 102 + export async function getOAuthSession( 103 + did: string, 104 + ): Promise<NodeSavedSession | undefined> { 105 + const store = await loadOAuthStore(); 106 + return store.sessions[did]; 107 + } 108 + 109 + /** 110 + * Delete an OAuth session by DID 111 + */ 112 + export async function deleteOAuthSession(did: string): Promise<boolean> { 113 + const store = await loadOAuthStore(); 114 + if (!store.sessions[did]) { 115 + return false; 116 + } 117 + delete store.sessions[did]; 118 + await saveOAuthStore(store); 119 + return true; 120 + } 121 + 122 + export function getOAuthStorePath(): string { 123 + return OAUTH_FILE; 124 + }
+34 -1
packages/cli/src/lib/types.ts
··· 37 bluesky?: BlueskyConfig; // Optional Bluesky posting configuration 38 } 39 40 - export interface Credentials { 41 pdsUrl: string; 42 identifier: string; 43 password: string; 44 } 45 46 export interface PostFrontmatter {
··· 37 bluesky?: BlueskyConfig; // Optional Bluesky posting configuration 38 } 39 40 + // Legacy credentials format (for backward compatibility during migration) 41 + export interface LegacyCredentials { 42 pdsUrl: string; 43 identifier: string; 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"; 77 } 78 79 export interface PostFrontmatter {

History

2 rounds 0 comments
sign up or login to add to the discussion
5 commits
expand
feat: initial oauth implementation
chore: cleaned up types
chore: updated icon styles
chore: updated og image
chore: updated docs
1/1 success
expand
expand 0 comments
pull request successfully merged
4 commits
expand
feat: initial oauth implementation
chore: cleaned up types
chore: updated icon styles
chore: updated og image
1/1 success
expand
expand 0 comments