Conversation
Agent-Logs-Url: https://github.com/humanwhocodes/crosspost/sessions/fe7dc41b-e70c-4d1f-8dd4-412544b96dd8 Co-authored-by: nzakas <38546+nzakas@users.noreply.github.com>
Agent-Logs-Url: https://github.com/humanwhocodes/crosspost/sessions/fe7dc41b-e70c-4d1f-8dd4-412544b96dd8 Co-authored-by: nzakas <38546+nzakas@users.noreply.github.com>
Agent-Logs-Url: https://github.com/humanwhocodes/crosspost/sessions/dd2e9c88-642c-4d17-94a6-28724d632e02 Co-authored-by: nzakas <38546+nzakas@users.noreply.github.com>
Added README instructions for obtaining a Reddit OAuth token, including a concrete token-generation example, required env vars, and message title/body behavior details. Addressed in commit |
nzakas
left a comment
There was a problem hiding this comment.
Thanks for putting this together. The shape follows the existing strategies: id/name, post(message, postOptions), validatePostOptions(), rejecting images the same way NostrStrategy does, signal passed to fetch, getUrlFromResponse, and MAX_MESSAGE_LENGTH/calculateMessageLength. The exports in src/index.ts, the CLI flag, and the README env var list are wired up too. The MCP server is strategy-agnostic, so it needs no changes. There are a few problems to address before this is usable, though. In order of severity:
- Access tokens expire after one hour. Per Reddit's OAuth2 docs, "All bearer tokens expire after 1 hour." Taking a static
REDDIT_ACCESS_TOKENmeans the CLI and MCP server stop working an hour after setup. The strategy should take long-lived credentials and get a token itself. For a script app that means client ID, client secret, username, and password withgrant_type=password. Alternatively, it could take a refresh token withgrant_type=refresh_token. See inline comments. - The README setup steps probably no longer work for new users. Reddit's Responsible Builder Policy (Nov 2025) ended self-service creation of new Data API credentials, and new apps now need manual approval. I couldn't load Reddit's own help page from here, so please verify, but the "click create another app" steps are likely out of date.
- The title is derived with no validation. Reddit caps titles at 300 characters. A single-line message becomes the whole title, which is the common crosspost case, so anything over 300 characters will be rejected by Reddit. A leading blank line produces an empty title.
MAX_MESSAGE_LENGTH/calculateMessageLengthdon't account for this, so the MCPcheck-message-lengthtool will report messages as fitting when they will fail. - Error handling.
response.json()runs before theokcheck, so a non-JSON error body turns into an opaqueSyntaxError. Reddit's non-json.errorserror bodies (e.g. 401{"message":"Unauthorized","error":401}) are reported as "Unknown Reddit API error." - The User-Agent version will go stale.
src/strategies/reddit.jsisn't inrelease-please-config.jsonextra-files, so thex-release-please-versionmarker is never updated. - Test gaps. Missing coverage for realistic auth errors, non-JSON bodies, title-only and CRLF messages, and the User-Agent header.
The core submit call is right: POST https://oauth.reddit.com/api/submit with api_type=json, kind=self, sr, title, text, and a check for json.errors on a 200 response.
| * @throws {Error} When options are missing. | ||
| */ | ||
| constructor(options) { | ||
| const { accessToken, subreddit } = options; |
There was a problem hiding this comment.
Reddit bearer tokens expire after one hour ("All bearer tokens expire after 1 hour" in the OAuth2 docs). With accessToken as the only credential, a user who follows the README gets a working setup for 60 minutes, and every later post fails with 401.
Suggestion: accept script-app credentials (clientId, clientSecret, username, password) and fetch a token lazily in post(). That means POST https://www.reddit.com/api/v1/access_token with HTTP Basic auth clientId:clientSecret, body grant_type=password&username=...&password=..., the same User-Agent, and the same signal. Cache the token until shortly before expires_in runs out. A refresh-token variant (grant_type=refresh_token) would also work if you'd rather not store the account password. Either way, the constructor validation, the RedditOptions typedef, the CLI env vars, and the README all need to follow.
| if (flags.reddit) { | ||
| strategies.push( | ||
| new RedditStrategy({ | ||
| accessToken: env.require("REDDIT_ACCESS_TOKEN"), |
There was a problem hiding this comment.
Because the token expires after an hour (see comment on reddit.js), a REDDIT_ACCESS_TOKEN env var doesn't suit a CLI/MCP server that people configure once in .env. Once the strategy takes long-lived credentials, this should become something like REDDIT_CLIENT_ID, REDDIT_CLIENT_SECRET, REDDIT_USERNAME, REDDIT_PASSWORD (or REDDIT_REFRESH_TOKEN), plus REDDIT_SUBREDDIT. Update the README env var list to match.
| } | ||
|
|
||
| const [firstLine, ...remainingLines] = message.split(/\r?\n/g); | ||
| const title = firstLine.trim(); |
There was a problem hiding this comment.
Title derivation needs validation:
- Reddit caps
titleat 300 characters. For a single-line message (the typical crosspost case, since the same text goes to Bluesky/Mastodon/etc.), the whole message becomes the title. Anything over 300 characters is rejected by Reddit after a round trip. - A message that starts with a newline (e.g.
"\nHello") produces an empty title, and Reddit rejects that too.
Suggestions: skip leading blank lines when picking the title, and throw a clear local error when the title is empty or longer than 300 code points. You could also fall back to using the full message as the body when the first line is too long, but that's a product decision.
| * @type {number} | ||
| * @const | ||
| */ | ||
| MAX_MESSAGE_LENGTH = 40300; |
There was a problem hiding this comment.
40300 (40,000-character body plus a 300-character title) is compared against calculateMessageLength(message), which counts the whole message. Because of the 300-character title limit, the MCP check-message-length tool will report a 1,000-character single-line message as fitting (1000/40300) when Reddit will actually reject it. The MCP resize-message flow relies on these values, so it would be good to make the length check reflect the title constraint. For example, calculateMessageLength could return a value that exceeds MAX_MESSAGE_LENGTH when the first line is over 300, or post() could at least fail fast with a clear message (see the comment on the title line).
| signal: postOptions?.signal, | ||
| }); | ||
|
|
||
| const result = /** @type {RedditSubmitResponse} */ (await response.json()); |
There was a problem hiding this comment.
response.json() runs before the response.ok check. When Reddit returns a non-JSON body (e.g. an HTML block/rate-limit page from the edge, or a 5xx), this throws a SyntaxError, and the user never sees the HTTP status.
Also, most OAuth-level errors don't use the json.errors shape. A bad or expired token returns 401 with {"message": "Unauthorized", "error": 401}, so the !response.ok branch reports "Unknown Reddit API error."
Suggestion: read the body as text first, try JSON.parse, and in the !ok branch fall back to body.message, or to the raw text when there's no JSON. On 429, including X-Ratelimit-Reset in the message would also help.
| Authorization: `Bearer ${this.#options.accessToken}`, | ||
| "Content-Type": "application/x-www-form-urlencoded", | ||
| "User-Agent": | ||
| "Crosspost (https://github.com/humanwhocodes/crosspost, v1.0.4)", // x-release-please-version |
There was a problem hiding this comment.
The // x-release-please-version marker only gets updated for files listed in release-please-config.json under extra-files. discord.js, discord-webhook.js, and telegram.js are listed there, but src/strategies/reddit.js isn't, so this will say v1.0.4 forever. Please add it to extra-files.
Minor: Reddit's API rules recommend <platform>:<app ID>:<version string> (by /u/<reddit username>) and say generic user agents get lower rate limits. The current string is honest and descriptive, so it's fine, but if you add a username option for the password grant you could use it here (e.g. nodejs:crosspost:v1.0.4 (by /u/<username>)). The README's curl example uses a different UA format than the code does.
|
|
||
| To enable posting to Reddit: | ||
|
|
||
| 1. Go to [Reddit Apps](https://www.reddit.com/prefs/apps) and click "create another app...". |
There was a problem hiding this comment.
Under Reddit's Responsible Builder Policy (published Nov 11, 2025), self-service creation of new Data API credentials was reportedly closed, and new apps now go through a manual approval request. I couldn't load the policy page on reddithelp.com directly, so please double-check, but if it holds, a new user following these steps won't get a client ID/secret. The README should at least note that approval is required, or that existing script-app credentials are needed.
|
|
||
| ```shell | ||
| curl -u "<CLIENT_ID>:<CLIENT_SECRET>" \ | ||
| -d "grant_type=password&username=<REDDIT_USERNAME>&password=<REDDIT_PASSWORD>" \ |
There was a problem hiding this comment.
Two issues with this step:
- The token this produces expires after one hour (
expires_in: 3600in the response), so copying it intoREDDIT_ACCESS_TOKENonly works briefly. This section will need rewriting once the strategy fetches its own token. - Passing the Reddit account password with
-don the command line leaves it in shell history and visible in the process list. If a manual curl step remains, suggest reading the credentials from env vars or a file (e.g.-d @body.txt). Also note that the password grant doesn't work on accounts with 2FA enabled unless the TOTP code is appended to the password, which makes it unusable for unattended use.
|
|
||
| it("should handle API errors", async () => { | ||
| server.post("/api/submit", { | ||
| status: 403, |
There was a problem hiding this comment.
This fixture (a 403 whose body carries json.errors with RATELIMIT) doesn't match what Reddit sends. RATELIMIT comes back as a 200 with json.errors, which the next test already covers. Non-2xx responses look like {"message": "Unauthorized", "error": 401}. Suggest replacing this with a realistic 401 case asserting the message is surfaced, plus a test where the error body isn't JSON (e.g. text/html) to cover the response.json() ordering issue.
| server.post( | ||
| { | ||
| url: "/api/submit", | ||
| headers: { |
There was a problem hiding this comment.
Coverage gaps worth adding here:
- Assert the
user-agentheader is sent. Reddit requires a descriptive UA, so a regression there matters. - A title-only message (
"Hello") should sendtext=empty. - A CRLF message (
"Title\r\nBody") splits correctly. - Title validation: a leading blank line, or a first line over 300 characters, should be rejected locally once that validation exists.
- If the token-fetch flow is added: token request body/auth header, token caching across two posts, and re-fetch after expiry.
REDDIT_ACCESS_TOKEN