Skip to content

feat(steam-link): serve /link, and expire the pending-link stash - #4880

Open
Celant wants to merge 6 commits into
mainfrom
josh/steam-link-entry-point
Open

feat(steam-link): serve /link, and expire the pending-link stash#4880
Celant wants to merge 6 commits into
mainfrom
josh/steam-link-entry-point

Conversation

@Celant

@Celant Celant commented Aug 5, 2026

Copy link
Copy Markdown
Member

Two small follow-ups to #4849 (Steam account linking), for the upcoming Steam release.

1. openfront.io/link serves the linking route

nginx.conf gains two exact-match blocks redirecting /link and /link/ to /#steam-link. The app is unchanged#steam-link stays the single internal route, and the client never needs to know /link exists.

Why it's worth a URL alias. When the desktop app can't hand the player's browser a URL, it shows an 8-character code and tells them where to type it. That is the one URL in the product a human types by hand — and they type it in a degraded situation, because the fallback fires precisely when this machine couldn't open a browser. The browser they end up using may be on another device; a phone is realistic, and # sits behind a symbol layer on a phone keyboard.

  • 302, not 301 — a permanent redirect is cached hard and would outlive any decision to retarget /link.
  • Both /link and /link/location = is exact-match, and browsers and address-bar autocomplete both append slashes. For a URL whose whole job is surviving being hand-typed, that variant isn't one to lose.
  • In nginx.conf rather than at the CDN edge — this is load-bearing for account linking, so it belongs somewhere version-controlled, reviewed and deployed with the app.

No dev-server change was needed: Vite's SPA fallback already serves /link.

2. The pending-link stash expires with its ticket

Logging in diverts the linking flow, so the intent is stashed in localStorage and resumed from onUserMe. It had no expiry, but the link ticket dies after 10 minutes server-side.

Without one, a player who starts linking, is sent to log in, and wanders off leaves an entry that fires weeks later — opening a confirmation modal for a long-dead token, which then errors. That's worse than doing nothing: the confirmation dialog is the security step of the whole feature (it's what stops someone on a shared machine linking the wrong account), and an unexplained prompt that always fails teaches players to dismiss it unread.

  • Entries carry a stashedAt and are discarded past the ticket TTL.
  • Consume-on-read is preserved on the expiry path too — an expired entry is removed as well as rejected, or it would be re-read and re-rejected on every page load forever.
  • Entries written before this change have no timestamp; their age is unknowable and their ticket almost certainly dead, so they're treated as expired.
  • The exported PendingLink type is unchanged — the timestamp is storage detail, invisible to callers.

The TTL comment now names the other two copies of that value (the server's, and the desktop gate's own), since there's no shared source to import across three deployables.

Verification

npx vitest run tests/client plus the translation gates → 879 tests passing. tsc --noEmit, ESLint and Prettier clean. nginx -t passes on the modified config (nginx:alpine, via Docker).

Note on the PR gate

No linked approved issue — internal Steam-release work rather than a community contribution, so it should clear on repo permission. Happy to file an issue and relink if you'd prefer.

Celant and others added 4 commits August 5, 2026 15:18
…nginx

devGameHtmlBypass is only wired as the bypass: option on the /w0 and /w1
proxy entries, so it's never invoked for a bare /link request -- the
/link branch added in the previous commit was dead code. Vite's dev
server already falls back to serving index.html for unmatched paths,
so /link gets 200 in dev with no change needed; the earlier curl check
couldn't tell the difference (a spot-check on made-up paths later
confirmed the same 200, which is what exposed this).

Reverting devGameHtmlBypass to its pre-existing state avoids config
that implies behavior it doesn't have.

Also add location = /link/ alongside the existing exact-match /link
redirect: `location =` doesn't match a trailing slash, and this URL
exists specifically to survive being hand-typed (or autocompleted)
into an address bar on an unfamiliar device.
The value is duplicated across three deployables (server, this client,
the desktop gate's gateFlow.js) with no shared source to import. The
comment explained why 10 minutes matters but not that changing it means
changing three files -- which is how the gate's own startup deadline got
mis-sized twice on this feature.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JRMRzHbZp2VhxbxqDMw4Zr
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3adb5947-db6b-4a2e-9a7e-088b92774fa9

📥 Commits

Reviewing files that changed from the base of the PR and between d3e70e1 and 44f1640.

📒 Files selected for processing (1)
  • vite.config.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • vite.config.ts

Walkthrough

The change adds production and development /link redirects to the Steam-link view. Pending Steam-link entries now store timestamps and expire after 10 minutes. Tests cover expiration, valid resumption, consumption, and legacy entries.

Changes

Steam-link flow

Layer / File(s) Summary
Link route redirects
nginx.conf, vite.config.ts
Exact-match /link and /link/ requests return temporary redirects to /#steam-link in production and development.
Pending-link TTL enforcement
src/client/SteamLink.ts, tests/client/SteamLink.test.ts
Token and code-entry stashes store stashedAt. takePendingLink rejects missing, invalid, or expired timestamps. Tests cover valid, expired, consumed, and timestamp-less entries.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Poem

/link redirects the way,
Pending tokens keep their time,
Ten minutes mark the limit,
Old entries leave when read,
Fresh links resume in line.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title directly describes both main changes: serving the /link route and expiring the pending-link stash.
Description check ✅ Passed The description thoroughly explains the rationale and implementation details for both features added in this pull request.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 5, 2026
A dev-only stand-in for nginx's `location = /link` blocks, in the same
spirit as the existing random-worker-create-proxy plugin.

Without it the dev server falls through to Vite's SPA fallback and
serves the home page for /link -- a 200, so it does not look broken,
but the URL the desktop app prints silently would not work locally.
Redirecting (rather than serving index.html directly) keeps dev and
production identical, so siteUrlForAudience emits one URL shape for
every environment instead of special-casing localhost.

Matches on `req.originalUrl`, not `req.url`, and that is load-bearing:
Vite's SPA fallback runs before this middleware and has already
rewritten `req.url` to "/index.html". A check against `req.url` compiles,
runs, and silently never matches. Found by logging both.

Verified against a running dev server: /link and /link/ both 302 to
/#steam-link; /linkxyz and / still 200, so the match is exact rather
than a prefix swallowing everything.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JRMRzHbZp2VhxbxqDMw4Zr

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
vite.config.ts (1)

71-78: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Correct the middleware-order explanation.

Vite runs configureServer middleware before its internal middleware. The SPA fallback does not normally rewrite req.url before this handler. Update this comment to describe the actual dependency. Keep originalUrl only if an earlier repository middleware rewrites req.url. (main.vite.dev)

Suggested comment correction
-      // Vite's SPA fallback runs BEFORE this middleware ...
-      // rewritten req.url to "/index.html" ...
+      // Use originalUrl if an earlier development middleware rewrites req.url.
🤖 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 `@vite.config.ts` around lines 71 - 78, Correct the middleware-order
explanation in the comment above the URL check: remove the claim that Vite’s SPA
fallback runs before this handler and rewrites req.url. Retain the use of
originalUrl only if an earlier repository middleware actually performs that
rewrite; otherwise update the surrounding implementation and comment to use the
appropriate request URL.

Source: MCP tools

🤖 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 `@vite.config.ts`:
- Around line 82-85: Update the Node middleware’s exact-route check around
URL.pathname to normalize the incoming URI using the same percent-encoded path
rules as Nginx before comparing against "/link" and "/link/". Ensure encoded
variants such as "/link%2F" resolve to the corresponding normalized route while
preserving the existing next() behavior for non-matching paths.

---

Nitpick comments:
In `@vite.config.ts`:
- Around line 71-78: Correct the middleware-order explanation in the comment
above the URL check: remove the claim that Vite’s SPA fallback runs before this
handler and rewrites req.url. Retain the use of originalUrl only if an earlier
repository middleware actually performs that rewrite; otherwise update the
surrounding implementation and comment to use the appropriate request URL.
🪄 Autofix

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: CHILL

Plan: Pro Plus

Run ID: 876b93e5-6b1b-4203-8812-999ce7235c80

📥 Commits

Reviewing files that changed from the base of the PR and between c6d45f1 and d3e70e1.

📒 Files selected for processing (1)
  • vite.config.ts

Comment thread vite.config.ts Outdated
@github-project-automation github-project-automation Bot moved this from Triage to Development in OpenFront Release Management Aug 5, 2026
Two points from review.

nginx resolves percent-encoded bytes before exact `location =` matching,
but URL.pathname does not. So /link%2F redirected in production and fell
straight through in dev -- the exact dev/prod divergence this plugin
exists to remove. Decode before comparing, and treat malformed encoding
as not-our-route rather than throwing.

Also corrected the comment above it. It asserted that Vite's SPA
fallback is what rewrites req.url before this handler runs. The rewrite
is real and measured (url="/index.html", originalUrl="/link"), but which
middleware performs it was never established -- that attribution was
inference presented as fact. The comment now states only what was
observed, and keeps the warning that matters: switching to req.url
type-checks, lints, runs, and silently never matches.

Verified against a running dev server: /link, /link/ and /link%2F all
302 to /#steam-link; /linkxyz, / and /link%2Fextra all still 200.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JRMRzHbZp2VhxbxqDMw4Zr
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Development

Development

Successfully merging this pull request may close these issues.

1 participant