Repository navigation
feat: cross post flow - #1
petrubraha wants to merge 7 commits into
Conversation
📝 WalkthroughWalkthroughAdds a Node.js Express demo for cross-posting content to Varful through OAuth2 Authorization Code with PKCE. The server validates environment configuration, accepts post data, redirects through authorization, exchanges tokens, forwards posts to the remote API, and renders post content into an HTML template. Supporting package metadata, setup documentation, environment examples, ignore rules, and a pre-filled browser form are included. Estimated code review effort: 3 (Moderate) | ~25 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@cross-posts/.gitignore`:
- Line 3: Remove package-lock.json from .gitignore and generate or retain the
npm lockfile in the repository so the OAuth demo’s dependency graph is committed
and reproducible.
In `@cross-posts/package.json`:
- Around line 7-10: Add an engines constraint to cross-posts/package.json
declaring Node.js 18 or newer, alongside the existing package metadata. Do not
alter the main entry point or dev script.
In `@cross-posts/partner.js`:
- Around line 236-241: Update the post rendering template in the post mapping
callback to HTML-escape post.location, post.user, and post.caption before
interpolation, and validate post.image so only https: URLs are used in the img
src attribute; reject or omit invalid image URLs rather than inserting them
directly.
- Around line 56-59: Replace the process-global codeVerifier and postData flow
state with per-session or cryptographically random state-keyed storage in the
authorization and callback handlers. Generate and include a state value in the
OAuth authorization URL, validate it on callback before exchanging the code,
retrieve the matching verifier and post payload, and delete only that flow’s
entry after use.
- Around line 108-117: Update the token exchange flow in the partner posts
handler around exchangeCredentialsForToken so the client-credentials token is
reused across feed requests instead of fetched every time. Store the token with
its expiry and refresh it only when expired or within a small refresh skew,
while preserving the existing credential exchange and fetchWithAccess flow;
align the reuse duration with the 10-hour requirement documented in the README.
- Around line 154-164: Apply the same finite timeout signal to all three
outbound fetch calls in cross-posts/partner.js: the client-credentials exchange
at lines 154-164, the authorization-code exchange at lines 179-193, and the
remote posts API request at lines 213-217. Add the bounded deadline to each
fetch options object while preserving their existing request behavior.
In `@cross-posts/README.md`:
- Around line 30-33: Update the “Configure Environment” instructions in
README.md to show `cp .env.example .env` as the primary macOS/Linux command, and
retain `copy .env.example .env` as the Windows alternative.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 5f0f1f66-f754-4e0a-b68f-0a1c40662454
📒 Files selected for processing (6)
cross-posts/.env.examplecross-posts/.gitignorecross-posts/README.mdcross-posts/package.jsoncross-posts/partner.jscross-posts/user.html
📜 Review details
🧰 Additional context used
🪛 ast-grep (0.44.1)
cross-posts/partner.js
[warning] 232-232: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.readFileSync(path.join(__dirname, "user.html"), "utf8")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').
(detect-non-literal-fs-filename)
🪛 dotenv-linter (4.0.0)
cross-posts/.env.example
[warning] 2-2: [QuoteCharacter] The value has quote characters (', ")
(QuoteCharacter)
[warning] 3-3: [QuoteCharacter] The value has quote characters (', ")
(QuoteCharacter)
[warning] 4-4: [QuoteCharacter] The value has quote characters (', ")
(QuoteCharacter)
[warning] 5-5: [QuoteCharacter] The value has quote characters (', ")
(QuoteCharacter)
🪛 GitHub Check: SonarCloud Code Analysis
cross-posts/partner.js
[warning] 36-36: This framework implicitly discloses version information by default. Make sure it is safe here.
[warning] 43-43: Make sure that enabling CORS is safe here.
🪛 markdownlint-cli2 (0.23.0)
cross-posts/README.md
[warning] 31-31: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
[warning] 33-33: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
[warning] 35-35: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
[warning] 40-40: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
[warning] 42-42: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
[warning] 44-44: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
[warning] 46-46: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
🔇 Additional comments (2)
cross-posts/.env.example (1)
1-5: LGTM!cross-posts/user.html (1)
1-41: LGTM!
| "type": "commonjs", | ||
| "main": "partner.js", | ||
| "scripts": { | ||
| "dev": "node partner.js" |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -eu
fd -HI -t f '^(package\.json|\.nvmrc|\.node-version|\.tool-versions|Dockerfile)$' .
rg -n -C2 '"engines"|FROM node:|NODE_VERSION|node-version' .Repository: petrubraha/varful.ro-api-demo
Length of output: 219
🌐 Web query:
For the Node.js version intended by this project, verify support for global fetch, btoa, and crypto.createHash(...).digest("base64url").
💡 Result:
As of July 2026, Node.js provides the following levels of support for the requested APIs: 1. Global fetch: Supported. It is available as a global object in Node.js, with the implementation originating from the Undici library [1][2]. It is no longer experimental, having been enabled by default since Node.js v18.0.0 and reaching stable status in v21.0.0 [3][1]. 2. Global btoa: Supported, but marked as legacy. While it is available as a global function, Node.js documentation categorizes it as legacy and recommends using Buffer.toString('base64') instead [4][5]. 3. crypto.createHash(...).digest("base64url"): Supported. The hash.digest method accepts 'base64url' as an encoding argument to return the digest in a URL-safe Base64 format [6][7][8]. This functionality is part of the standard node:crypto module [6].
Citations:
- 1: https://nodejs.org/api/globals.html
- 2: https://nodejs.org/learn/getting-started/fetch
- 3: https://nodejs.org/docs/latest-v23.x/api/globals.html
- 4: https://nodejs.org/api/globals.md
- 5: https://nodejs.org/docs/latest-v22.x/api/globals.html
- 6: https://nodejs.org/api/crypto.html
- 7: https://nodejs.org/docs/v22.11.0/api/crypto.html
- 8: https://nodejs.org/docs/v20.12.1/api/crypto.html
🏁 Script executed:
#!/bin/bash
set -eu
printf '--- candidate package/tooling files ---\n'
git ls-files | rg '(^|/)(package\.json|package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb|\.nvmrc|\.node-version|\.tool-versions|Dockerfile|docker-compose.*|.*\.yml|.*\.yaml)$' || true
printf '\n--- root package.json ---\n'
if [ -f package.json ]; then cat -n package.json; fi
printf '\n--- cross-posts package.json ---\n'
cat -n cross-posts/package.json
printf '\n--- root tooling/version references ---\n'
rg -n -C2 '"engines"|node:"?' package.json | head -n 80 || true
rg -n -C2 'node-version|setup-node|FROM node:|node:|NODE_VERSION|NODEJS_VERSION|node_modules|npm ci|npm install|devDependencies|dependencies' -g '*.yml' -g '*.yaml' -g 'Dockerfile*' -g 'package.json' . | head -n 160 || trueRepository: petrubraha/varful.ro-api-demo
Length of output: 258
🌐 Web query:
Node.js digest base64url support initial version createHash base64url
💡 Result:
In Node.js, the base64url encoding for digests was introduced in Node.js version 15.7.0 [1]. Before this official addition, Node.js had long supported base64url input when creating Buffers (via the base64 encoding option) [2][3], which was documented as a feature in 2016 [4][3]. However, developers could not explicitly request base64url output from cryptographic methods like hash.digest() or Buffer.toString() until the release of v15.7.0 [5][1]. Before this, developers typically had to perform manual string replacements—such as .toString('base64').replaceAll('+', '-').replaceAll('/', '_')—to achieve the desired output format [5]. Since v15.7.0, base64url is a first-class encoding option throughout the Node.js API, including the crypto module's hash.digest('base64url') and various Buffer operations [5][1].
Citations:
- 1: nodejs/node@5fe188e
- 2: Support for base64url format nodejs/node#26512
- 3: which variant of Base64 is officially supported? nodejs/node#5239
- 4: nodejs/node@1624d5b049
- 5: buffer: add base64url encoding option nodejs/node#36952
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- candidate package/tooling files ---'
git ls-files | rg '(^|/)(package\.json|package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb|\.nvmrc|\.node-version|\.tool-versions|Dockerfile|docker-compose.*|.*\.yml|.*\.yaml)$' || true
printf '%s\n' ''
printf '%s\n' '--- root package.json ---'
if [ -f package.json ]; then cat -n package.json; fi
printf '%s\n' ''
printf '%s\n' '--- cross-posts package.json ---'
cat -n cross-posts/package.json
printf '%s\n' ''
printf '%s\n' '--- root tooling/version references ---'
rg -n -C2 '"engines"|node:"?' package.json || true
rg -n -C2 'node-version|setup-node|FROM node:|node:|NODE_VERSION|NODEJS_VERSION|node_modules|npm ci|npm install|devDependencies|dependencies' -g '*.yml' -g '*.yaml' -g 'Dockerfile*' -g 'package.json' . || trueRepository: petrubraha/varful.ro-api-demo
Length of output: 1508
Declare the supported Node.js version.
cross-posts/package.json has no engines field, node tooling/version files, or CI Docker/runtime references. Since this script uses Node globals, add an engines constraint that covers Node 18+ so unsupported runtimes fail early.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@cross-posts/package.json` around lines 7 - 10, Add an engines constraint to
cross-posts/package.json declaring Node.js 18 or newer, alongside the existing
package metadata. Do not alter the main entry point or dev script.
| // Internal storage (in-memory) to link authorization redirects with original data. | ||
| let codeVerifier; | ||
| let codeChallenge; | ||
| let postData; |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
Bind each OAuth flow to its initiating browser session.
codeVerifier and postData are process-global: a second POST overwrites an in-flight flow, causing the first callback to exchange its code with the wrong verifier. Unauthenticated requests can repeatedly disrupt every active authorization. The authorization URL also lacks state, so the callback is not bound to its initiator.
Store the verifier and post payload per session (or keyed by a cryptographically random state), send state, validate it on callback, then delete only that flow’s entry.
Also applies to: 80-81, 122-130
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@cross-posts/partner.js` around lines 56 - 59, Replace the process-global
codeVerifier and postData flow state with per-session or cryptographically
random state-keyed storage in the authorization and callback handlers. Generate
and include a state value in the OAuth authorization URL, validate it on
callback before exchanging the code, retrieve the matching verifier and post
payload, and delete only that flow’s entry after use.
| if (!code) { | ||
| console.debug("[GET /partner/posts] Exchanging credentials for token..."); | ||
| const encodedCredentials = btoa( | ||
| `${config.clientId}:${config.clientSecret}`, | ||
| ); | ||
| const accessToken = await exchangeCredentialsForToken(encodedCredentials); | ||
|
|
||
| // const posts = mockPosts(); | ||
| console.debug("[GET /partner/posts] Retrieving posts with access token..."); | ||
| const posts = await fetchWithAccess(accessToken, "GET"); |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win
Cache the client-credentials token.
Every feed request calls the token endpoint. Cache the token with its expiry (and a small refresh skew) before fetching posts; this currently contradicts the 10-hour reuse requirement in cross-posts/README.md line 7.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@cross-posts/partner.js` around lines 108 - 117, Update the token exchange
flow in the partner posts handler around exchangeCredentialsForToken so the
client-credentials token is reused across feed requests instead of fetched every
time. Store the token with its expiry and refresh it only when expired or within
a small refresh skew, while preserving the existing credential exchange and
fetchWithAccess flow; align the reuse duration with the 10-hour requirement
documented in the README.
| const response = await fetch(config.varfulTokenUrl, { | ||
| method: "POST", | ||
| headers: { | ||
| Authorization: `Basic ${encodedCredentials}`, | ||
| "Cache-Control": "no-cache", | ||
| "Content-Type": "application/x-www-form-urlencoded", | ||
| }, | ||
| body: new URLSearchParams({ | ||
| grant_type: "client_credentials", | ||
| }), | ||
| }); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
For the Node.js version declared for this project, what supported mechanism cancels a global fetch request after a deadline?
💡 Result:
For the current Node.js versions (which include the v26.x and v24.x lines as of July 2026) [1][2], the supported mechanism to cancel a global fetch request after a specific deadline is the AbortSignal.timeout(ms) method [3][4]. This method, which is part of the Web API standard implemented in Node.js [5], creates an AbortSignal that automatically aborts after the specified duration in milliseconds [6][7]. You pass this signal to the fetch request via the signal property in the options object [6][4]. Example usage: const response = await fetch('https://api.example.com/data', { signal: AbortSignal.timeout(5000), // Cancels after 5000ms }); When the deadline is reached, the fetch request is automatically cancelled, and the promise rejects with a TimeoutError [6][7]. This approach is preferred over manually managing an AbortController and setTimeout, as it eliminates the need for manual cleanup and prevents memory leaks [3][6][4].
Citations:
- 1: https://nodejs.org/en/blog/vulnerability/july-2026-security-releases
- 2: https://github.com/nodejs/node/releases
- 3: https://realcoding.blog/en/2026/02/02/nodejs-abortcontroller-fetch-timeout/
- 4: https://techearl.com/fetch-timeout-abortcontroller
- 5: https://nodejs.org/api/globals.html
- 6: https://devcraftly.com/nodejs/abortcontroller/
- 7: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout_static
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== package files =="
git ls-files | grep -E '(^|/)(package.json|package-lock.json|pnpm-lock.yaml|yarn.lock|bun.lockb)$' || true
echo "== node engine / dependencies snippets =="
for f in $(git ls-files | grep -E '(^|/)package\.json$'); do
echo "--- $f"
sed -n '1,220p' "$f" | grep -nE '"(engines|version|exports|type|dependencies|devDependencies|peerDependencies|imports)"|fetch|AbortSignal|node-fetch|undici' -C 2 || true
done
echo "== cross-posts/partner.js relevant sections =="
if [ -f cross-posts/partner.js ]; then
wc -l cross-posts/partner.js
sed -n '130,230p' cross-posts/partner.js
echo "== fetch occurrences in cross-posts/partner.js =="
nl -ba cross-posts/partner.js | grep -n 'fetch\|Abort' -C 2 || true
else
echo "missing cross-posts/partner.js"
fi
echo "== lockfile env hints if present =="
for f in $(git ls-files | grep -E '(^|/)(package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb)$'); do
echo "--- $f"
grep -n 'node@|lts/|Node.js|nodejs.org|engines:' "$f" -C 1 | head -80 || true
doneRepository: petrubraha/varful.ro-api-demo
Length of output: 3940
Apply a finite timeout to every outbound OAuth/API request.
A stalled remote peer can leave these requests pending indefinitely and exhaust server resources. Add the same bounded deadline to the client-credentials exchange, authorization-code exchange, and remote posts API request, such as signal: AbortSignal.timeout(...) in each fetch.
📍 Affects 1 file
cross-posts/partner.js#L154-L164(this comment)cross-posts/partner.js#L179-L193cross-posts/partner.js#L213-L217
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@cross-posts/partner.js` around lines 154 - 164, Apply the same finite timeout
signal to all three outbound fetch calls in cross-posts/partner.js: the
client-credentials exchange at lines 154-164, the authorization-code exchange at
lines 179-193, and the remote posts API request at lines 213-217. Add the
bounded deadline to each fetch options object while preserving their existing
request behavior.
| (post) => ` | ||
| <div> | ||
| <p>${post.location}</p> | ||
| <p><strong>${post.user}</strong>: ${post.caption}</p> | ||
| <img src="${post.image}" width="100" /> | ||
| </div>`, |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Escape remote post content before rendering HTML.
location, user, and caption are inserted as HTML, while image is inserted into an attribute without validation. A post containing markup or an event-handler payload can execute in every feed viewer. HTML-escape text values and allow only validated https: image URLs before interpolation.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@cross-posts/partner.js` around lines 236 - 241, Update the post rendering
template in the post mapping callback to HTML-escape post.location, post.user,
and post.caption before interpolation, and validate post.image so only https:
URLs are used in the img src attribute; reject or omit invalid image URLs rather
than inserting them directly.
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@cross-posts/user.html`:
- Around line 12-13: Update the form action in the cross-posts template to avoid
hardcoding the localhost backend origin; use the Express-served relative partner
posts endpoint, or the configured partner API base URL when this page is
separately hosted, while preserving the POST target.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 17bf5ce6-08eb-4c3d-9d4a-1c8734fa41b4
📒 Files selected for processing (2)
cross-posts/README.mdcross-posts/user.html
📜 Review details
🔇 Additional comments (1)
cross-posts/README.md (1)
32-32: LGTM!
| <!-- localhost is your backend, the partner backend --> | ||
| <form action="http://localhost:3000/api/partner/posts" method="POST"> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Do not hardcode the backend origin in the form.
When this page is served outside http://localhost:3000—or over HTTPS—the browser submits to the visitor’s local machine instead of the partner backend. Use a relative action when the template is served by Express, or inject a configured partner API base URL for separately hosted pages.
Proposed fix
- <form action="http://localhost:3000/api/partner/posts" method="POST">
+ <form action="/api/partner/posts" method="POST">📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <!-- localhost is your backend, the partner backend --> | |
| <form action="http://localhost:3000/api/partner/posts" method="POST"> | |
| <!-- localhost is your backend, the partner backend --> | |
| <form action="/api/partner/posts" method="POST"> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@cross-posts/user.html` around lines 12 - 13, Update the form action in the
cross-posts template to avoid hardcoding the localhost backend origin; use the
Express-served relative partner posts endpoint, or the configured partner API
base URL when this page is separately hosted, while preserving the POST target.




Summary
cross-posts/mock Node.js “Varful Cross-Post Partner API Demo”.cross-posts/partner.js) showcasing a cross-post flow:POST /api/partner/posts: accepts a post payload, generates OAuth2 PKCEcode_verifier/code_challenge, then redirects the user to Varful’s/o/authorize/endpoint.GET /partner/posts: when called with?code=..., exchanges the authorizationcode+ PKCEcode_verifierfor an access token, forwards the previously submitted post to Varful (POST /api/posts/), then clears in-memory state.GET /partner/postswithout acode: fetches latest posts using a client-credentials token and renders them intocross-posts/user.html.cross-posts/user.html: a simple partner website form that submitsuser,image,location,location_slug, andcaptionto the partner backend.cross-posts/.env.examplewith required OAuth2 variables andVARFUL_HOSTcross-posts/.gitignoreto ignore secrets/depscross-posts/README.mdwith purpose, OAuth flow description, required fields, and run stepscross-posts/package.json(mock-partner,npm run devrunsnode partner.js)Security considerations
codeVerifier,codeChallenge,postData), which can break under concurrent users and are lost on restart.stateparameter: the authorization redirect does not include or validate an OAuth2statevalue, weakening CSRF/callback integrity guarantees.generateDynamicHtml()insertspost.location,post.user, andpost.captiondirectly into HTML without escaping;post.imageis also interpolated into an<img src=...>attribute without validation/escaping.req.bodyand forwards it to Varful without schema/type/length validation or sanitization.app.use(cors())enables default permissive CORS behavior.err.messageto the client and logs details to console.