Skip to content

Hosting: one-time exchange code for the signup session handoff - #1459

Merged
feruzm merged 4 commits into
developfrom
feature/hosting-handoff-exchange-code
Aug 12, 2026
Merged

Hosting: one-time exchange code for the signup session handoff#1459
feruzm merged 4 commits into
developfrom
feature/hosting-handoff-exchange-code

Conversation

@feruzm

@feruzm feruzm commented Aug 12, 2026

Copy link
Copy Markdown
Member

Closes #1449

What

The signup success screen's Customize link no longer carries the session bearer in its fragment. It carries a one-time short-TTL code minted by the hosting API, so a captured URL is worthless after a single exchange or five minutes, closing the browser-history and replay residuals accepted when #1448 shipped.

How

  • Hosting API: POST /v1/auth/handoff verifies the token against Hivesigner /me (the same shared resolver the login exchange uses), mints a nanoid code and stores { token, identity } in Redis with a five-minute TTL. POST /v1/auth/handoff/exchange trades the code for the session through an atomic GETDEL, so a code can only ever be exchanged once. Neither codes nor tokens reach the audit trail. Both endpoints sit under the existing auth rate limits.
  • ecency.com signup: the success screen mints on mount from the ensureValidToken-resolved token and re-mints on an interval while it stays open; the click opens #hc=<code>. A failed mint leaves the click on the credential-free fallback href, never a bearer substitute.
  • Instance: captures #hc= at boot (scrubbed pre-render, crash-safe decode like the bearer path), exchanges it once at the managed API and still applies the owner gate to whatever comes back. The #hs= bearer path stays for now: the web app promotes to production separately from the hosting stack, so instances must keep accepting bearer links until every deployed ecency.com sends codes.

Rollout order

Safe by construction: this merge deploys the hosting stack (API gains the endpoints, instances gain the code path) while ecency.com only starts minting after its own production promotion, against an API that already has the endpoint.

Tests

  • 4 hosting API tests (mint identity + TTL + audit hygiene, refuse invalid tokens, single-use exchange, expired/used 404)
  • 3 SPA tests (capture + scrub + one-shot exchange + owner gate, mismatched owner refused, malformed fragment safe) plus the existing bearer-path suite unchanged
  • 2 web specs updated/added (click carries the code and never the bearer; mint failure degrades to the credential-free href)
  • Full suites green: 2642 web, 927 SPA, 433 hosting API; typechecks and SPA production build

Summary by CodeRabbit

  • New Features

    • Added secure, short-lived handoff codes for moving from hosted signup to self-hosted setup.
    • Handoff codes are single-use, expire after five minutes, and replace access tokens in setup URLs.
    • Added automatic code exchange to establish the self-hosted session.
  • Bug Fixes

    • Improved handling of invalid, expired, malformed, or failed handoff requests.
    • Added safe fallback navigation when handoff creation fails.
  • Tests

    • Added coverage for successful exchanges, replay prevention, validation failures, expiration, and URL cleanup.

…doff

The Customize link used to carry the session bearer in its fragment,
which left a captured URL a live credential until upstream expiry. The
hosting API now mints a one-time code (nanoid, five-minute TTL, Redis
GETDEL so a code can only ever be exchanged once) for the
ensureValidToken-resolved token, the success screen re-mints on an
interval while it stays open and the link carries #hc=<code>. The
instance exchanges the code at the managed API and still applies its
owner gate to whatever comes back; the bearer #hs path stays until every
deployed ecency.com sends codes, since the web app promotes to production
separately from the hosting stack. Identity for minting comes from the
token via Hivesigner /me, shared with the login exchange, and neither
codes nor tokens reach the audit trail.
@qodo-code-review

qodo-code-review Bot commented Aug 12, 2026

Copy link
Copy Markdown

PR Summary by Qodo

Replace signup handoff bearer with one-time exchange code (TTL + single-use)

✨ Enhancement 🐞 Bug fix 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Mint short-lived one-time handoff codes instead of embedding session bearers in URLs.
• Exchange codes once at instance boot, then owner-gate the returned session.
• Add hosting API, instance SPA, and web signup specs covering success, failures, and replay safety.
Diagram

graph TD
  W["Web success screen"] -->|"mint code"| A["Hosting API auth"] --> HS{{"HiveSigner /me"}}
  A --> R[("Redis handoff store")]
  W -->|"opens #hc"| I["Instance bootstrap"] -->|"exchange once"| A
  I --> G{"Owner gate"} --> S["Authenticated session"]

  subgraph Legend
    direction LR
    _svc["Service"] ~~~ _ext{{"External"}} ~~~ _db[("Database") ] ~~~ _dec{"Decision"}
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Self-contained signed handoff token (JWT) instead of Redis-backed code
  • ➕ No Redis storage required for mint/exchange payload
  • ➕ Fewer moving parts operationally (no GETDEL dependency)
  • ➖ Hard to enforce strict single-use without server-side state (replay risk remains)
  • ➖ Still leaves a usable credential in URL history until expiry
2. One-time code bound to instance/tenant + PKCE-style verifier
  • ➕ Tighter binding reduces chance of code reuse across targets
  • ➕ Cleaner security story if multiple handoff destinations exist later
  • ➖ More parameters to pass and store (verifier/challenge), higher integration complexity
  • ➖ Current owner-gate already provides a strong destination check for this use case

Recommendation: Keep the current Redis-backed one-time code with short TTL and atomic consume: it directly addresses the residual replay/browser-history risk by making the URL artifact non-credential-bearing and strictly single-use. The existing owner gate remains the correct authorization boundary on the instance, and the staged rollout (keeping legacy #hs support) is pragmatic for independently deployed web vs hosting stacks.

Files changed (9) +486 / -56

Enhancement (6) +283 / -52
auth.tsAdd /auth/handoff mint + /auth/handoff/exchange and share HiveSigner identity resolver +120/-28

Add /auth/handoff mint + /auth/handoff/exchange and share HiveSigner identity resolver

• Extracts a shared HiveSigner /me identity resolution helper and reuses it for both login exchange and handoff minting. Adds a short-TTL code mint endpoint and a single-use exchange endpoint backed by Redis consume semantics, while keeping audit logs free of secrets.

apps/self-hosted/hosting/api/src/routes/auth.ts

redis.tsImplement Redis handoffStore with TTL set and atomic consume (GETDEL) +40/-1

Implement Redis handoffStore with TTL set and atomic consume (GETDEL)

• Adds a dedicated Redis store for handoff codes, storing {accessToken, username} under a namespaced key with an expiry. Provides an atomic consume that read-deletes the payload and validates parsed shape to enforce single-use semantics.

apps/self-hosted/hosting/api/src/utils/redis.ts

setup-handoff.tsSupport one-time handoff code flow alongside legacy bearer fragment +45/-6

Support one-time handoff code flow alongside legacy bearer fragment

• Adds parsing/storage for #hc codes, exchanges them via an injectable/default exchange function, and reuses the existing owner gate before accepting the returned session. Keeps the legacy #hs bearer path for rollout compatibility and ensures only one exchange attempt per page load.

apps/self-hosted/src/features/auth/setup-handoff.ts

handoff-exchange.tsAdd managed-API handoff code exchange client +43/-0

Add managed-API handoff code exchange client

• Introduces a small fetch helper to POST a handoff code to the managed hosting API exchange endpoint with a timeout and strict response validation. Returns null on failure to preserve the existing 'setup pending but logged out' behavior.

apps/self-hosted/src/features/auth/utils/handoff-exchange.ts

hosting-api.tsExpose hostingApi.mintHandoff for one-time code minting +8/-0

Expose hostingApi.mintHandoff for one-time code minting

• Adds a typed client method for POST /v1/auth/handoff to mint a short-lived handoff code used by the signup success screen. Returns the code, resolved username, and expiry timestamp.

apps/web/src/features/hosting-signup/hosting-api.ts

hosting-signup.tsxMint handoff codes on success screen and open Customize link with #hc=<code> +27/-17

Mint handoff codes on success screen and open Customize link with #hc=<code>

• Replaces bearer-in-fragment behavior with a one-time code minted from an ensureValidToken-resolved session token. Re-mints on an interval while the success screen stays open and falls back to the credential-free href if minting fails.

apps/web/src/features/hosting-signup/hosting-signup.tsx

Tests (3) +203 / -4
auth-handoff.test.tsAdd tests for handoff mint/exchange endpoints +103/-0

Add tests for handoff mint/exchange endpoints

• Introduces Vitest coverage for minting a one-time code from a HiveSigner token, refusing invalid tokens, exchanging a code exactly once, and returning 404 for missing/expired/used codes. Verifies audit logging does not leak codes or bearer tokens.

apps/self-hosted/hosting/api/src/routes/auth-handoff.test.ts

setup-handoff.test.tsExtend instance handoff tests to cover #hc code capture, exchange, and safety +65/-0

Extend instance handoff tests to cover #hc code capture, exchange, and safety

• Adds SPA tests for capturing and scrubbing #hc fragments, exchanging the code once, enforcing the owner gate on the exchanged identity, and handling malformed fragments without crashing. Confirms replay within a single load does not trigger multiple exchanges.

apps/self-hosted/src/features/auth/setup-handoff.test.ts

hosting-signup.spec.tsxUpdate/extend signup specs to assert code-in-fragment and safe fallback +35/-4

Update/extend signup specs to assert code-in-fragment and safe fallback

• Updates existing spec to assert Customize opens with #hc=<code> and that mintHandoff is called, never placing the bearer in the URL. Adds a new spec ensuring mint failures leave navigation on the credential-free href without opening a popup.

apps/web/src/specs/features/hosting-signup/hosting-signup.spec.tsx

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d2793974c0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1072 to +1075
if (!handoffCode) return;
e.preventDefault();
window.open(
`${safeBlogUrl}?setup=1#hs=${encodeURIComponent(handoffToken)}`,
`${safeBlogUrl}?setup=1#hc=${encodeURIComponent(handoffCode)}`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Invalidate the handoff code after opening it

After the first Customize click, the instance consumes this one-time code, but handoffCode remains unchanged until the four-minute refresh. Any second click during that window prevents the credential-free navigation and opens the already-used #hc URL again; its exchange returns 404, and this generated URL also omits the fallback login=hivesigner parameter, leaving the owner logged out. Clear and re-mint the code immediately after using it, or let subsequent clicks take the fallback href until a fresh code is available.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Confirmed and fixed in the follow-up commit: the click clears the code and bumps a mint nonce, so the instance-consumed code is never replayed (an immediate second click takes the credential-free default navigation while a fresh code mints), and the opened URL now carries the login=hivesigner fallback param so an unexchangeable code still lands a Hivesigner owner in a login flow. Spec pins the no-replay behavior.

The instance's exchange deletes the code, so the click clears it and
forces a fresh mint: an immediate second click takes the credential-free
default navigation instead of replaying a dead code into a logged-out
landing. The opened URL also carries the login=hivesigner fallback param,
so a code the instance cannot exchange still lands a Hivesigner owner in
a login flow.
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Aug 12, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (9) 📎 Requirement gaps (1) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Exchange skips /me validation ✓ Resolved 📎 Requirement gap ⛨ Security
Description
In the new #hc flow, the instance trusts the exchanged username from the hosting API and does
not re-resolve identity via HiveSigner /me for the returned accessToken. This materially alters
instance-side identity resolution and could weaken protections if the exchange response is incorrect
or compromised.
Code

apps/self-hosted/src/features/auth/setup-handoff.ts[R170-179]

+    if (code) {
+      // The code path: one exchange at the hosting API returns the session
+      // and the identity the API resolved from Hivesigner AT MINT TIME. The
+      // owner gate below still decides whether it may sign in here.
+      const exchanged = await (context.exchangeCode ?? exchangeHandoffCode)(
+        code,
+      );
+      if (!exchanged) return null;
+      account = exchanged.username.toLowerCase();
+      sessionToken = exchanged.accessToken;
Evidence
PR Compliance ID 5 requires preserving instance-side /me identity resolution behavior. The new
code path sets account = exchanged.username and sessionToken = exchanged.accessToken without
calling resolveHivesignerAccount(sessionToken) (or otherwise validating the token-to-username
binding) before applying the owner gate.

Preserve existing instance-side owner gate and /me identity resolution behavior
apps/self-hosted/src/features/auth/setup-handoff.ts[170-185]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The instance-side handoff for `#hc` accepts `{ accessToken, username }` from the hosting API and uses `username` for the owner gate without independently verifying the token/identity binding via HiveSigner `/me`.
## Issue Context
The compliance checklist requires preserving the existing instance-side owner gate and `/me` identity resolution behavior. The bearer-fragment path still uses `resolveHivesignerAccount(token)`, but the new code path bypasses it.
## Fix Focus Areas
- apps/self-hosted/src/features/auth/setup-handoff.ts[167-186]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. resolveHivesignerUsername uses any 📘 Rule violation ⚙ Maintainability
Description
resolveHivesignerUsername casts the HiveSigner /me response to any, introducing a new any
usage in TypeScript. This weakens type-safety and violates the project's no-any requirement.
Code

apps/self-hosted/hosting/api/src/routes/auth.ts[R163-164]

+    const data = (await res.json()) as any;
+    username = data?.account?.name ?? data?.user;
Relevance

●●● Strong

Team has accepted removing newly introduced any casts to keep TS strict.

PR-#1446
PR-#1285

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist forbids introducing any in changed TypeScript code. The new helper parses JSON using
an as any cast, which is a direct any introduction.

Rule 2668119: Disallow implicit and any types in new TypeScript code
apps/self-hosted/hosting/api/src/routes/auth.ts[161-165]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`resolveHivesignerUsername()` introduces `as any` when parsing the HiveSigner `/me` response, violating the rule against new `any` usage.
## Issue Context
The code already treats the parsed JSON as untrusted and validates `username` afterward, so it can safely use `unknown` and narrow types without resorting to `any`.
## Fix Focus Areas
- apps/self-hosted/hosting/api/src/routes/auth.ts[161-170]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. resolveHivesignerUsername uses any 📘 Rule violation ⚙ Maintainability
Description
resolveHivesignerUsername casts the HiveSigner /me response to any, introducing a new any
usage in TypeScript. This weakens type-safety and violates the project's no-any requirement.
Code

apps/self-hosted/hosting/api/src/routes/auth.ts[R163-164]

+    const data = (await res.json()) as any;
+    username = data?.account?.name ?? data?.user;
Relevance

●●● Strong

Team has accepted removing newly introduced any casts to keep TS strict.

PR-#1446
PR-#1285

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist forbids introducing any in changed TypeScript code. The new helper parses JSON using
an as any cast, which is a direct any introduction.

Rule 2668119: Disallow implicit and any types in new TypeScript code
apps/self-hosted/hosting/api/src/routes/auth.ts[161-165]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`resolveHivesignerUsername()` introduces `as any` when parsing the HiveSigner `/me` response, violating the rule against new `any` usage.
## Issue Context
The code already treats the parsed JSON as untrusted and validates `username` afterward, so it can safely use `unknown` and narrow types without resorting to `any`.
## Fix Focus Areas
- apps/self-hosted/hosting/api/src/routes/auth.ts[161-170]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. resolveHivesignerUsername uses any 📘 Rule violation ⚙ Maintainability
Description
resolveHivesignerUsername casts the HiveSigner /me response to any, introducing a new any
usage in TypeScript. This weakens type-safety and violates the project's no-any requirement.
Code

apps/self-hosted/hosting/api/src/routes/auth.ts[R163-164]

+    const data = (await res.json()) as any;
+    username = data?.account?.name ?? data?.user;
Relevance

●●● Strong

Team has accepted removing newly introduced any casts to keep TS strict.

PR-#1446
PR-#1285

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist forbids introducing any in changed TypeScript code. The new helper parses JSON using
an as any cast, which is a direct any introduction.

Rule 2668119: Disallow implicit and any types in new TypeScript code
apps/self-hosted/hosting/api/src/routes/auth.ts[161-165]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`resolveHivesignerUsername()` introduces `as any` when parsing the HiveSigner `/me` response, violating the rule against new `any` usage.
## Issue Context
The code already treats the parsed JSON as untrusted and validates `username` afterward, so it can safely use `unknown` and narrow types without resorting to `any`.
## Fix Focus Areas
- apps/self-hosted/hosting/api/src/routes/auth.ts[161-170]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View medium (10)
5. handoffCode minted before click 📎 Requirement gap ⛨ Security
Description
The signup success screen mints #hc codes on mount and periodically, not at click time as
required, increasing the window where an unused code can be captured and exchanged. This deviates
from the compliance requirement that the handoff code be minted at click time for the Customize
link.
Code

apps/web/src/features/hosting-signup/hosting-signup.tsx[R414-419]

+    const mint = async () => {
+      try {
+        const token = await ensureValidToken(activeUser.username);
+        if (!token || cancelled) return;
+        const minted = await hostingApi.mintHandoff(token);
+        if (!cancelled) setHandoffCode(minted.code);
Evidence
PR Compliance ID 1 requires the handoff code to be minted at click time; the new code explicitly
mints inside a useEffect on success-screen mount and refreshes every 4 minutes, meaning the code
can exist before the user clicks Customize.

Replace session token URL fragment with one-time handoff code in Customize link
apps/web/src/features/hosting-signup/hosting-signup.tsx[402-429]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The compliance checklist requires minting the one-time handoff code at click time, but the current implementation mints it on success-screen mount (and refreshes it on an interval).
## Issue Context
The current `useEffect` calls `ensureValidToken()` and `hostingApi.mintHandoff()` ahead of the click, then the click handler uses the cached `handoffCode`. This is intentional for UX/popup-blocker reasons, but it violates the stated compliance success criteria.
## Fix Focus Areas
- apps/web/src/features/hosting-signup/hosting-signup.tsx[402-430]
- apps/web/src/features/hosting-signup/hosting-signup.tsx[1068-1076]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. No per-account handoff limit ✓ Resolved 📎 Requirement gap ⛨ Security
Description
The new /v1/auth/handoff mint endpoint is only covered by existing per-IP rate limiting, with no
additional rate limit keyed by the resolved account. This violates the requirement to rate limit
minting by both account and IP, and enables high-volume minting spread across many accounts behind
one IP (or vice versa).
Code

apps/self-hosted/hosting/api/src/routes/auth.ts[R221-228]

+    const resolved = await resolveHivesignerUsername(accessToken);
+    if (!resolved.ok) {
+      return c.json({ error: resolved.error }, resolved.status);
+    }
+    const { username } = resolved;
+
+    const code = nanoid(32);
+    await handoffStore.set(code, { accessToken, username }, HANDOFF_TTL_SECONDS);
Evidence
PR Compliance ID 4 requires per-account and per-IP rate limiting for minting. The app-level limiter
for /v1/auth/* is explicitly per-IP, and the new /handoff route does not add any per-account
throttling after resolving username.

Rate limit handoff code minting by account and IP
apps/self-hosted/hosting/api/src/routes/auth.ts[215-229]
apps/self-hosted/hosting/api/src/index.ts[59-77]
apps/self-hosted/hosting/api/src/middleware/rate-limit.ts[28-61]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`POST /v1/auth/handoff` lacks rate limiting by authenticated account; only per-IP rate limiting is applied at the app level.
## Issue Context
Compliance requires rate limiting by both account and IP. The app currently applies `rateLimit({ name: 'auth', ... })` which keys solely by trusted client IP; the new handoff mint route does not add any account-based budget.
## Fix Focus Areas
- apps/self-hosted/hosting/api/src/routes/auth.ts[207-244]
- apps/self-hosted/hosting/api/src/index.ts[59-77]
- apps/self-hosted/hosting/api/src/middleware/rate-limit.ts[28-82]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Consumed code remains clickable ✓ Resolved 🐞 Bug ≡ Correctness
Description
After the first successful exchange, HostingSignup retains and reuses the same handoffCode on
later Customize clicks even though Redis has atomically deleted it. Subsequent clicks before a newer
code is successfully minted open a destination whose exchange returns 404, leaving the owner logged
out instead of carrying the session.
Code

apps/web/src/features/hosting-signup/hosting-signup.tsx[R1072-1075]

+                if (!handoffCode) return;
             e.preventDefault();
             window.open(
-                  `${safeBlogUrl}?setup=1#hs=${encodeURIComponent(handoffToken)}`,
+                  `${safeBlogUrl}?setup=1#hc=${encodeURIComponent(handoffCode)}`,
Evidence
The click handler always opens the current state value and never clears it; the only normal
replacement is the four-minute interval. The API calls handoffStore.consume, returns 404 when it
yields no payload, and the store implements consumption with Redis GETDEL, proving the first
successful destination exchange invalidates later uses.

apps/web/src/features/hosting-signup/hosting-signup.tsx[414-425]
apps/web/src/features/hosting-signup/hosting-signup.tsx[1072-1078]
apps/self-hosted/hosting/api/src/routes/auth.ts[260-263]
apps/self-hosted/hosting/api/src/utils/redis.ts[130-136]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The Customize click handler continues using a one-time handoff code after the destination consumes it. A retry, double-click, or second tab therefore exchanges an already-deleted code and loses the session handoff.
## Issue Context
Redis consumption uses atomic `GETDEL`, and the API returns 404 for a consumed code. Ensure each launch gets a fresh code or that the consumed code is immediately removed and safely reminted while preserving popup-blocker behavior and the credential-free fallback.
## Fix Focus Areas
- apps/web/src/features/hosting-signup/hosting-signup.tsx[414-425]
- apps/web/src/features/hosting-signup/hosting-signup.tsx[1072-1078]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Refreshed code can expire before UI updates ✓ Resolved 🐞 Bug ☼ Reliability
Description
The success screen re-mints a handoff code every 4 minutes, but the currently displayed code (with a
5-minute server TTL) is only replaced once ensureValidToken() and hostingApi.mintHandoff() both
resolve; if that round trip takes longer than the ~1-minute margin, a click on Customize opens the
URL with the now-expired code, causing the instance-side exchange to fail with a 404 instead of
falling back to the credential-free href.
Code

apps/web/src/features/hosting-signup/hosting-signup.tsx[R414-425]

+    const mint = async () => {
+      try {
+        const token = await ensureValidToken(activeUser.username);
+        if (!token || cancelled) return;
+        const minted = await hostingApi.mintHandoff(token);
+        if (!cancelled) setHandoffCode(minted.code);
+      } catch {
+        if (!cancelled) setHandoffCode(null);
+      }
+    };
+    mint();
+    const timer = setInterval(mint, 4 * 60 * 1000);
Evidence
HANDOFF_TTL_SECONDS is 5*60 in auth.ts (server), while the client re-mint interval is set to
4*60*1000 ms; the click handler at line 1072 only checks if (!handoffCode) return, without
checking freshness/expiry, so a stale (but still non-null) handoffCode from a delayed mint cycle
will be used in the URL even after its server-side TTL has elapsed.

apps/web/src/features/hosting-signup/hosting-signup.tsx[411-430]
apps/self-hosted/hosting/api/src/routes/auth.ts[213-244]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The handoff code minted on the signup success screen expires 5 minutes after mint (server-side `HANDOFF_TTL_SECONDS = 5 * 60`), but the client only re-mints a fresh code on a fixed 4-minute interval, and the currently-held `handoffCode` state is not replaced until the async `ensureValidToken()` + `hostingApi.mintHandoff()` round trip resolves. If that round trip is slow (network latency, retries, a busy hosting API), the previous code can pass its 5-minute TTL before the new one is stored, and a user click during that window will open the Customize link with an already-expired code, causing the instance-side `/v1/auth/handoff/exchange` call to fail (404) with no automatic fallback.
## Issue Context
- Server TTL: `apps/self-hosted/hosting/api/src/routes/auth.ts` `HANDOFF_TTL_SECONDS = 5 * 60`.
- Client re-mint interval: `apps/web/src/features/hosting-signup/hosting-signup.tsx` `setInterval(mint, 4 * 60 * 1000)`.
- The click handler only checks `if (!handoffCode) return;` with no expiry awareness before opening the URL.
## Fix Focus Areas
- apps/web/src/features/hosting-signup/hosting-signup.tsx[410-430]
- apps/web/src/features/hosting-signup/hosting-signup.tsx[1067-1079]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


9. Consumed code remains clickable 🐞 Bug ≡ Correctness
Description
HostingSignup keeps using the same handoffCode after opening it, although the API deletes that
code on its first exchange. A second click or retry therefore opens an already-used code and leaves
the instance logged out until a replacement is minted.
Code

apps/web/src/features/hosting-signup/hosting-signup.tsx[R1072-1075]

+                if (!handoffCode) return;
             e.preventDefault();
             window.open(
-                  `${safeBlogUrl}?setup=1#hs=${encodeURIComponent(handoffToken)}`,
+                  `${safeBlogUrl}?setup=1#hc=${encodeURIComponent(handoffCode)}`,
Relevance

●● Moderate

UX/flow decision (clearing code after click) without strong repo precedent; could be debated.

PR-#1448

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The click handler repeatedly reads unchanged React state, while the server's consuming read deletes
the Redis key before returning the session; the route consequently returns 404 for the reused code.

apps/web/src/features/hosting-signup/hosting-signup.tsx[1072-1078]
apps/self-hosted/hosting/api/src/utils/redis.ts[130-136]
apps/self-hosted/hosting/api/src/routes/auth.ts[260-263]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The Customize handler retains a one-time handoff code after opening it, so subsequent clicks reuse a code that Redis has already consumed.
## Issue Context
The exchange endpoint uses atomic GETDEL. After opening a code URL, clear or disable that code and mint a replacement for a later retry; add a multiple-click test.
## Fix Focus Areas
- apps/web/src/features/hosting-signup/hosting-signup.tsx[1072-1078]
- apps/web/src/specs/features/hosting-signup/hosting-signup.spec.tsx[188-203]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


10. Refresh retains expired code ✓ Resolved 🐞 Bug ☼ Reliability
Description
When ensureValidToken() returns undefined, the periodic mint exits without clearing the previous
code from client state. Once that code's five-minute Redis TTL expires, the Customize action remains
a dead handoff until a later interval successfully replaces it.
Code

apps/web/src/features/hosting-signup/hosting-signup.tsx[R416-419]

+        const token = await ensureValidToken(activeUser.username);
+        if (!token || cancelled) return;
+        const minted = await hostingApi.mintHandoff(token);
+        if (!cancelled) setHandoffCode(minted.code);
Relevance

●● Moderate

Behavioral/state-management change; no clear precedent on clearing handoff state when refresh fails.

PR-#1022

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The mint function only clears state in its catch branch, but token renewal failures are converted
into an undefined return; Redis independently expires the retained client code after 300 seconds.

apps/web/src/features/hosting-signup/hosting-signup.tsx[414-425]
apps/web/src/utils/user-token.ts[141-159]
apps/self-hosted/hosting/api/src/routes/auth.ts[213-241]
apps/self-hosted/hosting/api/src/utils/redis.ts[125-127]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The no-token branch bypasses the catch handler and leaves an old handoff code visible after it expires server-side.
## Issue Context
`ensureValidToken()` resolves to `undefined` on refresh failure rather than throwing. Clear `handoffCode` before returning when no token is available, and test a failed scheduled refresh after an earlier successful mint.
## Fix Focus Areas
- apps/web/src/features/hosting-signup/hosting-signup.tsx[414-425]
- apps/web/src/specs/features/hosting-signup/hosting-signup.spec.tsx[205-226]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


11. Consumed code remains clickable ✓ Resolved 🐞 Bug ≡ Correctness
Description
HostingSignup keeps using the same handoffCode after opening it, although the API deletes that
code on its first exchange. A second click or retry therefore opens an already-used code and leaves
the instance logged out until a replacement is minted.
Code

apps/web/src/features/hosting-signup/hosting-signup.tsx[R1072-1075]

+                if (!handoffCode) return;
              e.preventDefault();
              window.open(
-                  `${safeBlogUrl}?setup=1#hs=${encodeURIComponent(handoffToken)}`,
+                  `${safeBlogUrl}?setup=1#hc=${encodeURIComponent(handoffCode)}`,
Relevance

●● Moderate

UX/flow decision (clearing code after click) without strong repo precedent; could be debated.

PR-#1448

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The click handler repeatedly reads unchanged React state, while the server's consuming read deletes
the Redis key before returning the session; the route consequently returns 404 for the reused code.

apps/web/src/features/hosting-signup/hosting-signup.tsx[1072-1078]
apps/self-hosted/hosting/api/src/utils/redis.ts[130-136]
apps/self-hosted/hosting/api/src/routes/auth.ts[260-263]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The Customize handler retains a one-time handoff code after opening it, so subsequent clicks reuse a code that Redis has already consumed.
## Issue Context
The exchange endpoint uses atomic GETDEL. After opening a code URL, clear or disable that code and mint a replacement for a later retry; add a multiple-click test.
## Fix Focus Areas
- apps/web/src/features/hosting-signup/hosting-signup.tsx[1072-1078]
- apps/web/src/specs/features/hosting-signup/hosting-signup.spec.tsx[188-203]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


12. Refresh retains expired code ✓ Resolved 🐞 Bug ☼ Reliability
Description
When ensureValidToken() returns undefined, the periodic mint exits without clearing the previous
code from client state. Once that code's five-minute Redis TTL expires, the Customize action remains
a dead handoff until a later interval successfully replaces it.
Code

apps/web/src/features/hosting-signup/hosting-signup.tsx[R416-419]

+        const token = await ensureValidToken(activeUser.username);
+        if (!token || cancelled) return;
+        const minted = await hostingApi.mintHandoff(token);
+        if (!cancelled) setHandoffCode(minted.code);
Relevance

●● Moderate

Behavioral/state-management change; no clear precedent on clearing handoff state when refresh fails.

PR-#1022

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The mint function only clears state in its catch branch, but token renewal failures are converted
into an undefined return; Redis independently expires the retained client code after 300 seconds.

apps/web/src/features/hosting-signup/hosting-signup.tsx[414-425]
apps/web/src/utils/user-token.ts[141-159]
apps/self-hosted/hosting/api/src/routes/auth.ts[213-241]
apps/self-hosted/hosting/api/src/utils/redis.ts[125-127]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The no-token branch bypasses the catch handler and leaves an old handoff code visible after it expires server-side.
## Issue Context
`ensureValidToken()` resolves to `undefined` on refresh failure rather than throwing. Clear `handoffCode` before returning when no token is available, and test a failed scheduled refresh after an earlier successful mint.
## Fix Focus Areas
- apps/web/src/features/hosting-signup/hosting-signup.tsx[414-425]
- apps/web/src/specs/features/hosting-signup/hosting-signup.spec.tsx[205-226]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


13. Consumed code remains clickable ✓ Resolved 🐞 Bug ≡ Correctness
Description
HostingSignup keeps using the same handoffCode after opening it, although the API deletes that
code on its first exchange. A second click or retry therefore opens an already-used code and leaves
the instance logged out until a replacement is minted.
Code

apps/web/src/features/hosting-signup/hosting-signup.tsx[R1072-1075]

+                if (!handoffCode) return;
               e.preventDefault();
               window.open(
-                  `${safeBlogUrl}?setup=1#hs=${encodeURIComponent(handoffToken)}`,
+                  `${safeBlogUrl}?setup=1#hc=${encodeURIComponent(handoffCode)}`,
Relevance

●● Moderate

UX/flow decision (clearing code after click) without strong repo precedent; could be debated.

PR-#1448

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The click handler repeatedly reads unchanged React state, while the server's consuming read deletes
the Redis key before returning the session; the route consequently returns 404 for the reused code.

apps/web/src/features/hosting-signup/hosting-signup.tsx[1072-1078]
apps/self-hosted/hosting/api/src/utils/redis.ts[130-136]
apps/self-hosted/hosting/api/src/routes/auth.ts[260-263]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The Customize handler retains a one-time handoff code after opening it, so subsequent clicks reuse a code that Redis has already consumed.
## Issue Context
The exchange endpoint uses atomic GETDEL. After opening a code URL, clear or disable that code and mint a replacement for a later retry; add a multiple-click test.
## Fix Focus Areas
- apps/web/src/features/hosting-signup/hosting-signup.tsx[1072-1078]
- apps/web/src/specs/features/hosting-signup/hosting-signup.spec.tsx[188-203]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


14. Refresh retains expired code ✓ Resolved 🐞 Bug ☼ Reliability
Description
When ensureValidToken() returns undefined, the periodic mint exits without clearing the previous
code from client state. Once that code's five-minute Redis TTL expires, the Customize action remains
a dead handoff until a later interval successfully replaces it.
Code

apps/web/src/features/hosting-signup/hosting-signup.tsx[R416-419]

+        const token = await ensureValidToken(activeUser.username);
+        if (!token || cancelled) return;
+        const minted = await hostingApi.mintHandoff(token);
+        if (!cancelled) setHandoffCode(minted.code);
Relevance

●● Moderate

Behavioral/state-management change; no clear precedent on clearing handoff state when refresh fails.

PR-#1022

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The mint function only clears state in its catch branch, but token renewal failures are converted
into an undefined return; Redis independently expires the retained client code after 300 seconds.

apps/web/src/features/hosting-signup/hosting-signup.tsx[414-425]
apps/web/src/utils/user-token.ts[141-159]
apps/self-hosted/hosting/api/src/routes/auth.ts[213-241]
apps/self-hosted/hosting/api/src/utils/redis.ts[125-127]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The no-token branch bypasses the catch handler and leaves an old handoff code visible after it expires server-side.
## Issue Context
`ensureValidToken()` resolves to `undefined` on refresh failure rather than throwing. Clear `handoffCode` before returning when no token is available, and test a failed scheduled refresh after an earlier successful mint.
## Fix Focus Areas
- apps/web/src/features/hosting-signup/hosting-signup.tsx[414-425]
- apps/web/src/specs/features/hosting-signup/hosting-signup.spec.tsx[205-226]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

15. Duplicated hardcoded managed API base URL 🐞 Bug ⚙ Maintainability
Description
handoff-exchange.ts hardcodes the full exchange endpoint URL
(https://api.blogs.ecency.com/hosting/v1/auth/handoff/exchange) as a separate literal instead of
reusing the existing HOSTING_API_URL constant already defined in floating-menu-window.tsx,
creating duplicated configuration that can silently drift if the managed API base ever changes or
needs environment overrides.
Code

apps/self-hosted/src/features/auth/utils/handoff-exchange.ts[R10-11]

+const HANDOFF_EXCHANGE_URL =
+  'https://api.blogs.ecency.com/hosting/v1/auth/handoff/exchange';
Evidence
floating-menu-window.tsx already defines `const HOSTING_API_URL =
'https://api.blogs.ecency.com/hosting'; and builds requests as ${HOSTING_API_URL}/v1/...`;
handoff-exchange.ts instead defines its own separate full-URL constant rather than composing from a
single shared base, meaning any change to the managed API host must be made in two places.

apps/self-hosted/src/features/floating-menu/components/floating-menu-window.tsx[32-32]
apps/self-hosted/src/features/auth/utils/handoff-exchange.ts[10-12]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`handoff-exchange.ts` hardcodes its own full URL literal for the managed hosting API's exchange endpoint, duplicating the existing `HOSTING_API_URL` base constant already defined elsewhere in the codebase for the same host. This duplication risks configuration drift if the managed API's address ever changes (e.g. staging environment, migration), since a developer updating one constant could easily miss the other.
## Issue Context
`floating-menu-window.tsx` already defines `const HOSTING_API_URL = 'https://api.blogs.ecency.com/hosting';` and composes paths like `${HOSTING_API_URL}/v1/tenants/...`. The new `handoff-exchange.ts` instead defines its own separate full literal `HANDOFF_EXCHANGE_URL`.
## Fix Focus Areas
- apps/self-hosted/src/features/auth/utils/handoff-exchange.ts[10-12]
- apps/self-hosted/src/features/floating-menu/components/floating-menu-window.tsx[32-32]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


16. Global fetch stub not restored 📘 Rule violation ▣ Testability
Description
The new test stubs the global fetch via vi.stubGlobal but never restores it, which can leak
global state into other tests and cause order-dependent failures. Tests should explicitly undo
global mutations after each case.
Code

apps/self-hosted/hosting/api/src/routes/auth-handoff.test.ts[R47-50]

+  beforeEach(() => {
+    vi.clearAllMocks();
+    vi.stubGlobal('fetch', mocks.fetch);
+    mocks.fetch.mockResolvedValue(ME_OK);
Relevance

● Weak

Similar request to restore global fetch spy was rejected; team seems okay relying on suite-wide mock
reset.

PR-#1456

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The rule requires cleanup of global mutations in tests. The test suite stubs fetch in
beforeEach, and the file ends without any afterEach (or equivalent) to restore globals.

Rule 2668097: Tests must restore global state (DOM, timers, mocks) after each test
apps/self-hosted/hosting/api/src/routes/auth-handoff.test.ts[46-52]
apps/self-hosted/hosting/api/src/routes/auth-handoff.test.ts[98-103]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A test file stubs the global `fetch` but does not restore it after each test, risking cross-test contamination.
## Issue Context
`vi.stubGlobal('fetch', ...)` persists until explicitly undone (e.g., `vi.unstubAllGlobals()`), and `vi.clearAllMocks()` does not restore original global implementations.
## Fix Focus Areas
- apps/self-hosted/hosting/api/src/routes/auth-handoff.test.ts[46-52]
- apps/self-hosted/hosting/api/src/routes/auth-handoff.test.ts[98-103]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


17. window.open spy not restored 📘 Rule violation ▣ Testability
Description
The new spec creates a vi.spyOn(window, 'open') mock but does not restore it, leaving global
behavior modified for subsequent tests. This can cause flaky failures and violates the test cleanup
requirement.
Code

apps/web/src/specs/features/hosting-signup/hosting-signup.spec.tsx[R223-225]

+    const open = vi.spyOn(window, "open").mockImplementation(() => null);
+    fireEvent.click(customize);
+    expect(open).not.toHaveBeenCalled();
Relevance

● Weak

Exact suggestion (restore window.open spy) was previously rejected for this same spec file.

PR-#1448

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist requires tests to restore global state after each test. The added test mocks
window.open and ends without restoring it.

Rule 2668097: Tests must restore global state (DOM, timers, mocks) after each test
apps/web/src/specs/features/hosting-signup/hosting-signup.spec.tsx[205-226]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A test spies on `window.open` with a mocked implementation but does not restore it, mutating a global for later tests.
## Issue Context
`vi.clearAllMocks()` in `beforeEach` clears call history but does not restore original implementations for spies; use `open.mockRestore()`/`vi.restoreAllMocks()` in `afterEach`.
## Fix Focus Areas
- apps/web/src/specs/features/hosting-signup/hosting-signup.spec.tsx[205-226]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View low (4)
18. Global fetch stub not restored 📘 Rule violation ▣ Testability
Description
The new test stubs the global fetch via vi.stubGlobal but never restores it, which can leak
global state into other tests and cause order-dependent failures. Tests should explicitly undo
global mutations after each case.
Code

apps/self-hosted/hosting/api/src/routes/auth-handoff.test.ts[R47-50]

+  beforeEach(() => {
+    vi.clearAllMocks();
+    vi.stubGlobal('fetch', mocks.fetch);
+    mocks.fetch.mockResolvedValue(ME_OK);
Relevance

● Weak

Similar request to restore global fetch spy was rejected; team seems okay relying on suite-wide mock
reset.

PR-#1456

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The rule requires cleanup of global mutations in tests. The test suite stubs fetch in
beforeEach, and the file ends without any afterEach (or equivalent) to restore globals.

Rule 2668097: Tests must restore global state (DOM, timers, mocks) after each test
apps/self-hosted/hosting/api/src/routes/auth-handoff.test.ts[46-52]
apps/self-hosted/hosting/api/src/routes/auth-handoff.test.ts[98-103]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A test file stubs the global `fetch` but does not restore it after each test, risking cross-test contamination.
## Issue Context
`vi.stubGlobal('fetch', ...)` persists until explicitly undone (e.g., `vi.unstubAllGlobals()`), and `vi.clearAllMocks()` does not restore original global implementations.
## Fix Focus Areas
- apps/self-hosted/hosting/api/src/routes/auth-handoff.test.ts[46-52]
- apps/self-hosted/hosting/api/src/routes/auth-handoff.test.ts[98-103]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


19. window.open spy not restored 📘 Rule violation ▣ Testability
Description
The new spec creates a vi.spyOn(window, 'open') mock but does not restore it, leaving global
behavior modified for subsequent tests. This can cause flaky failures and violates the test cleanup
requirement.
Code

apps/web/src/specs/features/hosting-signup/hosting-signup.spec.tsx[R223-225]

+    const open = vi.spyOn(window, "open").mockImplementation(() => null);
+    fireEvent.click(customize);
+    expect(open).not.toHaveBeenCalled();
Relevance

● Weak

Exact suggestion (restore window.open spy) was previously rejected for this same spec file.

PR-#1448

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist requires tests to restore global state after each test. The added test mocks
window.open and ends without restoring it.

Rule 2668097: Tests must restore global state (DOM, timers, mocks) after each test
apps/web/src/specs/features/hosting-signup/hosting-signup.spec.tsx[205-226]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A test spies on `window.open` with a mocked implementation but does not restore it, mutating a global for later tests.
## Issue Context
`vi.clearAllMocks()` in `beforeEach` clears call history but does not restore original implementations for spies; use `open.mockRestore()`/`vi.restoreAllMocks()` in `afterEach`.
## Fix Focus Areas
- apps/web/src/specs/features/hosting-signup/hosting-signup.spec.tsx[205-226]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


20. Global fetch stub not restored 📘 Rule violation ▣ Testability
Description
The new test stubs the global fetch via vi.stubGlobal but never restores it, which can leak
global state into other tests and cause order-dependent failures. Tests should explicitly undo
global mutations after each case.
Code

apps/self-hosted/hosting/api/src/routes/auth-handoff.test.ts[R47-50]

+  beforeEach(() => {
+    vi.clearAllMocks();
+    vi.stubGlobal('fetch', mocks.fetch);
+    mocks.fetch.mockResolvedValue(ME_OK);
Relevance

● Weak

Similar request to restore global fetch spy was rejected; team seems okay relying on suite-wide mock
reset.

PR-#1456

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The rule requires cleanup of global mutations in tests. The test suite stubs fetch in
beforeEach, and the file ends without any afterEach (or equivalent) to restore globals.

Rule 2668097: Tests must restore global state (DOM, timers, mocks) after each test
apps/self-hosted/hosting/api/src/routes/auth-handoff.test.ts[46-52]
apps/self-hosted/hosting/api/src/routes/auth-handoff.test.ts[98-103]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A test file stubs the global `fetch` but does not restore it after each test, risking cross-test contamination.
## Issue Context
`vi.stubGlobal('fetch', ...)` persists until explicitly undone (e.g., `vi.unstubAllGlobals()`), and `vi.clearAllMocks()` does not restore original global implementations.
## Fix Focus Areas
- apps/self-hosted/hosting/api/src/routes/auth-handoff.test.ts[46-52]
- apps/self-hosted/hosting/api/src/routes/auth-handoff.test.ts[98-103]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


21. window.open spy not restored 📘 Rule violation ▣ Testability
Description
The new spec creates a vi.spyOn(window, 'open') mock but does not restore it, leaving global
behavior modified for subsequent tests. This can cause flaky failures and violates the test cleanup
requirement.
Code

apps/web/src/specs/features/hosting-signup/hosting-signup.spec.tsx[R223-225]

+    const open = vi.spyOn(window, "open").mockImplementation(() => null);
+    fireEvent.click(customize);
+    expect(open).not.toHaveBeenCalled();
Relevance

● Weak

Exact suggestion (restore window.open spy) was previously rejected for this same spec file.

PR-#1448

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist requires tests to restore global state after each test. The added test mocks
window.open and ends without restoring it.

Rule 2668097: Tests must restore global state (DOM, timers, mocks) after each test
apps/web/src/specs/features/hosting-signup/hosting-signup.spec.tsx[205-226]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A test spies on `window.open` with a mocked implementation but does not restore it, mutating a global...

Comment thread apps/web/src/features/hosting-signup/hosting-signup.tsx Outdated
Comment on lines +221 to +228
const resolved = await resolveHivesignerUsername(accessToken);
if (!resolved.ok) {
return c.json({ error: resolved.error }, resolved.status);
}
const { username } = resolved;

const code = nanoid(32);
await handoffStore.set(code, { accessToken, username }, HANDOFF_TTL_SECONDS);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

2. No per-account handoff limit 📎 Requirement gap ⛨ Security

The new /v1/auth/handoff mint endpoint is only covered by existing per-IP rate limiting, with no
additional rate limit keyed by the resolved account. This violates the requirement to rate limit
minting by both account and IP, and enables high-volume minting spread across many accounts behind
one IP (or vice versa).
Agent Prompt
## Issue description
`POST /v1/auth/handoff` lacks rate limiting by authenticated account; only per-IP rate limiting is applied at the app level.

## Issue Context
Compliance requires rate limiting by both account and IP. The app currently applies `rateLimit({ name: 'auth', ... })` which keys solely by trusted client IP; the new handoff mint route does not add any account-based budget.

## Fix Focus Areas
- apps/self-hosted/hosting/api/src/routes/auth.ts[207-244]
- apps/self-hosted/hosting/api/src/index.ts[59-77]
- apps/self-hosted/hosting/api/src/middleware/rate-limit.ts[28-82]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Confirmed and fixed in 948d5ac: minting now counts against the resolved account in a rolling minute (Redis INCR with expiry) beside the per-IP limits, capped well above legitimate use (one code per success screen plus a slow refresh). Route test covers the cap.

Comment thread apps/self-hosted/src/features/auth/setup-handoff.ts
Comment on lines +1072 to +1075
if (!handoffCode) return;
e.preventDefault();
window.open(
`${safeBlogUrl}?setup=1#hs=${encodeURIComponent(handoffToken)}`,
`${safeBlogUrl}?setup=1#hc=${encodeURIComponent(handoffCode)}`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

4. Consumed code remains clickable 🐞 Bug ≡ Correctness

After the first successful exchange, HostingSignup retains and reuses the same handoffCode on
later Customize clicks even though Redis has atomically deleted it. Subsequent clicks before a newer
code is successfully minted open a destination whose exchange returns 404, leaving the owner logged
out instead of carrying the session.
Agent Prompt
## Issue description
The Customize click handler continues using a one-time handoff code after the destination consumes it. A retry, double-click, or second tab therefore exchanges an already-deleted code and loses the session handoff.

## Issue Context
Redis consumption uses atomic `GETDEL`, and the API returns 404 for a consumed code. Ensure each launch gets a fresh code or that the consumed code is immediately removed and safely reminted while preserving popup-blocker behavior and the credential-free fallback.

## Fix Focus Areas
- apps/web/src/features/hosting-signup/hosting-signup.tsx[414-425]
- apps/web/src/features/hosting-signup/hosting-signup.tsx[1072-1078]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This ran against the pre-fix head: since bcec696 the click clears the code and forces a fresh mint, an immediate second click takes the credential-free default navigation and the opened URL carries the login=hivesigner fallback. Spec pins the no-replay behavior.

Comment on lines +414 to +425
const mint = async () => {
try {
const token = await ensureValidToken(activeUser.username);
if (!token || cancelled) return;
const minted = await hostingApi.mintHandoff(token);
if (!cancelled) setHandoffCode(minted.code);
} catch {
if (!cancelled) setHandoffCode(null);
}
};
mint();
const timer = setInterval(mint, 4 * 60 * 1000);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

5. Refreshed code can expire before ui updates 🐞 Bug ☼ Reliability

The success screen re-mints a handoff code every 4 minutes, but the currently displayed code (with a
5-minute server TTL) is only replaced once ensureValidToken() and hostingApi.mintHandoff() both
resolve; if that round trip takes longer than the ~1-minute margin, a click on Customize opens the
URL with the now-expired code, causing the instance-side exchange to fail with a 404 instead of
falling back to the credential-free href.
Agent Prompt
## Issue description
The handoff code minted on the signup success screen expires 5 minutes after mint (server-side `HANDOFF_TTL_SECONDS = 5 * 60`), but the client only re-mints a fresh code on a fixed 4-minute interval, and the currently-held `handoffCode` state is not replaced until the async `ensureValidToken()` + `hostingApi.mintHandoff()` round trip resolves. If that round trip is slow (network latency, retries, a busy hosting API), the previous code can pass its 5-minute TTL before the new one is stored, and a user click during that window will open the Customize link with an already-expired code, causing the instance-side `/v1/auth/handoff/exchange` call to fail (404) with no automatic fallback.

## Issue Context
- Server TTL: `apps/self-hosted/hosting/api/src/routes/auth.ts` `HANDOFF_TTL_SECONDS = 5 * 60`.
- Client re-mint interval: `apps/web/src/features/hosting-signup/hosting-signup.tsx` `setInterval(mint, 4 * 60 * 1000)`.
- The click handler only checks `if (!handoffCode) return;` with no expiry awareness before opening the URL.

## Fix Focus Areas
- apps/web/src/features/hosting-signup/hosting-signup.tsx[410-430]
- apps/web/src/features/hosting-signup/hosting-signup.tsx[1067-1079]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread apps/self-hosted/src/features/auth/utils/handoff-exchange.ts
@qodo-code-review

qodo-code-review Bot commented Aug 12, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (3) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. resolveHivesignerUsername uses any 📘 Rule violation ⚙ Maintainability
Description
resolveHivesignerUsername casts the HiveSigner /me response to any, introducing a new any
usage in TypeScript. This weakens type-safety and violates the project's no-any requirement.
Code

apps/self-hosted/hosting/api/src/routes/auth.ts[R163-164]

+    const data = (await res.json()) as any;
+    username = data?.account?.name ?? data?.user;
Relevance

●●● Strong

Team has accepted removing newly introduced any casts to keep TS strict.

PR-#1446
PR-#1285

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist forbids introducing any in changed TypeScript code. The new helper parses JSON using
an as any cast, which is a direct any introduction.

Rule 2668119: Disallow implicit and any types in new TypeScript code
apps/self-hosted/hosting/api/src/routes/auth.ts[161-165]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`resolveHivesignerUsername()` introduces `as any` when parsing the HiveSigner `/me` response, violating the rule against new `any` usage.

## Issue Context
The code already treats the parsed JSON as untrusted and validates `username` afterward, so it can safely use `unknown` and narrow types without resorting to `any`.

## Fix Focus Areas
- apps/self-hosted/hosting/api/src/routes/auth.ts[161-170]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Consumed code remains clickable ✓ Resolved 🐞 Bug ≡ Correctness
Description
HostingSignup keeps using the same handoffCode after opening it, although the API deletes that
code on its first exchange. A second click or retry therefore opens an already-used code and leaves
the instance logged out until a replacement is minted.
Code

apps/web/src/features/hosting-signup/hosting-signup.tsx[R1072-1075]

+                if (!handoffCode) return;
                e.preventDefault();
                window.open(
-                  `${safeBlogUrl}?setup=1#hs=${encodeURIComponent(handoffToken)}`,
+                  `${safeBlogUrl}?setup=1#hc=${encodeURIComponent(handoffCode)}`,
Relevance

●● Moderate

UX/flow decision (clearing code after click) without strong repo precedent; could be debated.

PR-#1448

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The click handler repeatedly reads unchanged React state, while the server's consuming read deletes
the Redis key before returning the session; the route consequently returns 404 for the reused code.

apps/web/src/features/hosting-signup/hosting-signup.tsx[1072-1078]
apps/self-hosted/hosting/api/src/utils/redis.ts[130-136]
apps/self-hosted/hosting/api/src/routes/auth.ts[260-263]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The Customize handler retains a one-time handoff code after opening it, so subsequent clicks reuse a code that Redis has already consumed.

## Issue Context
The exchange endpoint uses atomic GETDEL. After opening a code URL, clear or disable that code and mint a replacement for a later retry; add a multiple-click test.

## Fix Focus Areas
- apps/web/src/features/hosting-signup/hosting-signup.tsx[1072-1078]
- apps/web/src/specs/features/hosting-signup/hosting-signup.spec.tsx[188-203]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Refresh retains expired code ✓ Resolved 🐞 Bug ☼ Reliability
Description
When ensureValidToken() returns undefined, the periodic mint exits without clearing the previous
code from client state. Once that code's five-minute Redis TTL expires, the Customize action remains
a dead handoff until a later interval successfully replaces it.
Code

apps/web/src/features/hosting-signup/hosting-signup.tsx[R416-419]

+        const token = await ensureValidToken(activeUser.username);
+        if (!token || cancelled) return;
+        const minted = await hostingApi.mintHandoff(token);
+        if (!cancelled) setHandoffCode(minted.code);
Relevance

●● Moderate

Behavioral/state-management change; no clear precedent on clearing handoff state when refresh fails.

PR-#1022

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The mint function only clears state in its catch branch, but token renewal failures are converted
into an undefined return; Redis independently expires the retained client code after 300 seconds.

apps/web/src/features/hosting-signup/hosting-signup.tsx[414-425]
apps/web/src/utils/user-token.ts[141-159]
apps/self-hosted/hosting/api/src/routes/auth.ts[213-241]
apps/self-hosted/hosting/api/src/utils/redis.ts[125-127]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The no-token branch bypasses the catch handler and leaves an old handoff code visible after it expires server-side.

## Issue Context
`ensureValidToken()` resolves to `undefined` on refresh failure rather than throwing. Clear `handoffCode` before returning when no token is available, and test a failed scheduled refresh after an earlier successful mint.

## Fix Focus Areas
- apps/web/src/features/hosting-signup/hosting-signup.tsx[414-425]
- apps/web/src/specs/features/hosting-signup/hosting-signup.spec.tsx[205-226]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

4. Global fetch stub not restored 📘 Rule violation ▣ Testability
Description
The new test stubs the global fetch via vi.stubGlobal but never restores it, which can leak
global state into other tests and cause order-dependent failures. Tests should explicitly undo
global mutations after each case.
Code

apps/self-hosted/hosting/api/src/routes/auth-handoff.test.ts[R47-50]

+  beforeEach(() => {
+    vi.clearAllMocks();
+    vi.stubGlobal('fetch', mocks.fetch);
+    mocks.fetch.mockResolvedValue(ME_OK);
Relevance

● Weak

Similar request to restore global fetch spy was rejected; team seems okay relying on suite-wide mock
reset.

PR-#1456

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The rule requires cleanup of global mutations in tests. The test suite stubs fetch in
beforeEach, and the file ends without any afterEach (or equivalent) to restore globals.

Rule 2668097: Tests must restore global state (DOM, timers, mocks) after each test
apps/self-hosted/hosting/api/src/routes/auth-handoff.test.ts[46-52]
apps/self-hosted/hosting/api/src/routes/auth-handoff.test.ts[98-103]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A test file stubs the global `fetch` but does not restore it after each test, risking cross-test contamination.

## Issue Context
`vi.stubGlobal('fetch', ...)` persists until explicitly undone (e.g., `vi.unstubAllGlobals()`), and `vi.clearAllMocks()` does not restore original global implementations.

## Fix Focus Areas
- apps/self-hosted/hosting/api/src/routes/auth-handoff.test.ts[46-52]
- apps/self-hosted/hosting/api/src/routes/auth-handoff.test.ts[98-103]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. window.open spy not restored 📘 Rule violation ▣ Testability
Description
The new spec creates a vi.spyOn(window, 'open') mock but does not restore it, leaving global
behavior modified for subsequent tests. This can cause flaky failures and violates the test cleanup
requirement.
Code

apps/web/src/specs/features/hosting-signup/hosting-signup.spec.tsx[R223-225]

+    const open = vi.spyOn(window, "open").mockImplementation(() => null);
+    fireEvent.click(customize);
+    expect(open).not.toHaveBeenCalled();
Relevance

● Weak

Exact suggestion (restore window.open spy) was previously rejected for this same spec file.

PR-#1448

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist requires tests to restore global state after each test. The added test mocks
window.open and ends without restoring it.

Rule 2668097: Tests must restore global state (DOM, timers, mocks) after each test
apps/web/src/specs/features/hosting-signup/hosting-signup.spec.tsx[205-226]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A test spies on `window.open` with a mocked implementation but does not restore it, mutating a global for later tests.

## Issue Context
`vi.clearAllMocks()` in `beforeEach` clears call history but does not restore original implementations for spies; use `open.mockRestore()`/`vi.restoreAllMocks()` in `afterEach`.

## Fix Focus Areas
- apps/web/src/specs/features/hosting-signup/hosting-signup.spec.tsx[205-226]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context
✅ Compliance rules (platform): 75 rules
✅ Skills: 6 invoked
  add-feature
  add-query
  add-sdk-mutation
  add-test
  code-review
  debug
✅ Web pages:
  +8 more
Review mode: 🧠 Deep: This security-sensitive handoff changes multiple independent API, Redis atomicity, SPA authentication, and signup flows, creating a dense set of subtle cross-path defects where redundant review is materially valuable.

Grey Divider

Tip of the day
💡 Did you know, you can enable the Remediation agent and Qodo fixes findings in a dedicated fix PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread apps/self-hosted/hosting/api/src/routes/auth.ts
Comment thread apps/web/src/features/hosting-signup/hosting-signup.tsx Outdated
Comment thread apps/web/src/features/hosting-signup/hosting-signup.tsx Outdated
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@feruzm, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 44 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9a6450eb-5e79-4e9b-8ad0-d62bb590089e

📥 Commits

Reviewing files that changed from the base of the PR and between d279397 and 5efa5c0.

📒 Files selected for processing (7)
  • apps/self-hosted/hosting/api/src/routes/auth-handoff.test.ts
  • apps/self-hosted/hosting/api/src/routes/auth.ts
  • apps/self-hosted/hosting/api/src/utils/redis.ts
  • apps/self-hosted/src/features/auth/setup-handoff.test.ts
  • apps/self-hosted/src/features/auth/setup-handoff.ts
  • apps/web/src/features/hosting-signup/hosting-signup.tsx
  • apps/web/src/specs/features/hosting-signup/hosting-signup.spec.tsx
📝 Walkthrough

Walkthrough

The PR adds short-lived, single-use handoff codes for hosting signup. The API stores codes in Redis, the web signup flow passes codes instead of bearer tokens, and self-hosted setup exchanges codes into authenticated sessions.

Changes

Hosting handoff flow

Layer / File(s) Summary
API minting and consumption
apps/self-hosted/hosting/api/src/routes/auth.ts, apps/self-hosted/hosting/api/src/utils/redis.ts, apps/self-hosted/hosting/api/src/routes/auth-handoff.test.ts
The API resolves HiveSigner identity, mints five-minute codes, stores token and username payloads, and consumes codes once. Tests cover success, rejected validation, missing codes, storage, TTL, and audit contents.
Self-hosted code capture and session creation
apps/self-hosted/src/features/auth/setup-handoff.ts, apps/self-hosted/src/features/auth/utils/handoff-exchange.ts, apps/self-hosted/src/features/auth/setup-handoff.test.ts
Self-hosted setup captures and scrubs #hc fragments, exchanges codes with timeout and payload validation, applies owner checks, and handles replay or malformed codes.
Signup minting and navigation
apps/web/src/features/hosting-signup/hosting-api.ts, apps/web/src/features/hosting-signup/hosting-signup.tsx, apps/web/src/specs/features/hosting-signup/hosting-signup.spec.tsx
The signup flow refreshes handoff codes periodically and places hc= in the customization URL fragment. Minting failures use a credential-free fallback URL.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Signup as hosting signup
  participant HostingAPI as hostingApi
  participant AuthAPI as hosting auth API
  participant Redis as handoffStore
  participant Setup as self-hosted setup
  Signup->>HostingAPI: mintHandoff with access token
  HostingAPI->>AuthAPI: POST /v1/auth/handoff
  AuthAPI->>Redis: Store five-minute handoff payload
  AuthAPI-->>HostingAPI: Return one-time code
  Signup-->>Setup: Navigate with `#hc` code
  Setup->>AuthAPI: POST /handoff/exchange
  AuthAPI->>Redis: Consume code once
  Redis-->>AuthAPI: Return token and username
  AuthAPI-->>Setup: Return session payload
  Setup-->>Setup: Validate owner and create session
Loading

Possibly related PRs

  • ecency/vision-web#1448 — Introduced the session-handoff flow that this PR changes from bearer tokens to single-use codes.
  • ecency/vision-web#1438 — Modified the signup customization flow used by this handoff integration.

Suggested labels: patch

Poem

A rabbit hops with codes in tow,
Five minutes make the tokens go.
One mint, one use, then safely gone,
The signup path keeps hopping on.
“No bearer crumbs!” the rabbit sings.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The implementation meets most requirements in [#1449], but no per-account and per-IP mint rate limiting is shown. Add rate limiting for handoff-code minting by account and IP, and add tests for both limits.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the primary change: replacing the signup session bearer-token handoff with a one-time exchange code.
Out of Scope Changes check ✅ Passed The API, client, instance exchange flow, and related tests all support the handoff-code requirements in [#1449].
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/hosting-handoff-exchange-code

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

🤖 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 `@apps/self-hosted/hosting/api/src/routes/auth.ts`:
- Around line 215-244: Update the /handoff handler around authRoutes.post and
add per-account and per-IP mint rate limiting: check and reject an exhausted IP
budget before resolveHivesignerUsername, then check the resolved username’s
budget before nanoid creation and handoffStore.set, returning 429 without
writing a handoff code. In apps/self-hosted/hosting/api/src/routes/auth.ts lines
215-244, implement the route changes; in
apps/self-hosted/hosting/api/src/routes/auth-handoff.test.ts lines 46-103, add
isolated tests covering exhausted IP and account limits plus successful minting
for distinct accounts and IPs.

In `@apps/web/src/features/hosting-signup/hosting-signup.tsx`:
- Around line 410-430: Update the handoff-code effect around ensureValidToken
and mintHandoff to depend on activeUser?.username, clearing handoffCode when
step is not "success", no active user exists, or before each mint attempt. Also
clear it when ensureValidToken returns no token, while preserving cancellation
handling so codes cannot persist across logout or account changes.

In `@apps/web/src/specs/features/hosting-signup/hosting-signup.spec.tsx`:
- Around line 188-198: Update the hosting signup test around the mintHandoff
success flow to await the initial mintHandoff promise and state update before
interacting with the customize control. Move fireEvent.click(customize) outside
waitFor, click exactly once, then assert the expected open URL and window
features.
🪄 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: 587525d8-e6f8-4cd4-827b-0422986fd277

📥 Commits

Reviewing files that changed from the base of the PR and between 69c270d and d279397.

📒 Files selected for processing (9)
  • apps/self-hosted/hosting/api/src/routes/auth-handoff.test.ts
  • apps/self-hosted/hosting/api/src/routes/auth.ts
  • apps/self-hosted/hosting/api/src/utils/redis.ts
  • apps/self-hosted/src/features/auth/setup-handoff.test.ts
  • apps/self-hosted/src/features/auth/setup-handoff.ts
  • apps/self-hosted/src/features/auth/utils/handoff-exchange.ts
  • apps/web/src/features/hosting-signup/hosting-api.ts
  • apps/web/src/features/hosting-signup/hosting-signup.tsx
  • apps/web/src/specs/features/hosting-signup/hosting-signup.spec.tsx

Comment thread apps/self-hosted/hosting/api/src/routes/auth.ts
Comment thread apps/web/src/features/hosting-signup/hosting-signup.tsx Outdated
Comment on lines 188 to 198
// The minted code resolves asynchronously on success-screen mount, so
// retry the click until the state lands. Only the one-time code travels
// in the URL; the bearer stays out of it entirely.
await waitFor(() => {
fireEvent.click(customize);
expect(open).toHaveBeenCalledWith(
"https://alice.blogs.ecency.com/?setup=1#hs=tok-alice",
"https://alice.blogs.ecency.com/?setup=1#hc=hand-off-code-1234567890abcdef",
"_blank",
"noopener,noreferrer"
);
});

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- target file outline ---'
ast-grep outline apps/web/src/specs/features/hosting-signup/hosting-signup.spec.tsx || true
printf '%s\n' '--- target test context ---'
sed -n '1,240p' apps/web/src/specs/features/hosting-signup/hosting-signup.spec.tsx
printf '%s\n' '--- related symbols ---'
rg -n -C 4 'mintHandoff|customize|window\.open|waitFor' apps/web/src/specs/features/hosting-signup apps/web/src | head -300

Repository: ecency/vision-web

Length of output: 43896


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- hosting signup candidates ---'
fd -i 'hosting-signup' apps/web/src
printf '%s\n' '--- component symbols ---'
rg -n -C 8 'mintHandoff|customize-your-blog|window\.open|onClick|handoff|hand-off' apps/web/src/features/hosting-signup
printf '%s\n' '--- component outline ---'
component=$(fd -i -t f 'hosting-signup.tsx' apps/web/src | head -1)
if [ -n "${component}" ]; then
  ast-grep outline "$component" || true
  printf '%s\n' '--- component size ---'
  wc -l "$component"
fi

Repository: ecency/vision-web

Length of output: 48190


🏁 Script executed:

#!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path

test = Path("apps/web/src/specs/features/hosting-signup/hosting-signup.spec.tsx").read_text()
component = Path("apps/web/src/features/hosting-signup/hosting-signup.tsx").read_text()

assert "await waitFor(() => {\n      fireEvent.click(customize);" in test
assert "const minted = await hostingApi.mintHandoff(token);" in component
assert "if (!handoffCode) return;" in component
assert "setHandoffCode(minted.code)" in component

# Minimal model of Testing Library's retry behavior: the callback is retried
# until its assertion passes. A click is a side effect on every invocation.
handoff_ready = False
clicks = 0
opens = 0
for attempt in range(2):
    clicks += 1
    if handoff_ready:
        opens += 1
        break
    # The initial mint resolves between retries and React applies the state.
    handoff_ready = True

assert clicks == 2
assert opens == 1
print("source pattern: click is inside waitFor")
print("component pattern: window.open requires handoffCode set after mintHandoff resolves")
print(f"retry model: clicks={clicks}, opens={opens}")
print("call observation alone does not establish that the resolved code is in component state")
PY

Repository: ecency/vision-web

Length of output: 396


Move the click outside waitFor

Wait for the initial mintHandoff promise to resolve and the code to reach component state. Then click once and assert the popup 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 `@apps/web/src/specs/features/hosting-signup/hosting-signup.spec.tsx` around
lines 188 - 198, Update the hosting signup test around the mintHandoff success
flow to await the initial mintHandoff promise and state update before
interacting with the customize control. Move fireEvent.click(customize) outside
waitFor, click exactly once, then assert the expected open URL and window
features.

Source: Coding guidelines

feruzm added 2 commits August 12, 2026 18:15
…doff codes

Minting counts against the account inside a rolling minute beside the
per-IP limits, so a leaked token cannot become a code mill (legitimate use
is one code per success screen plus a slow refresh). The instance
re-resolves the exchanged session against Hivesigner /me itself and
requires the identity to match, so the account it signs in was never
taken on the API's word alone; the owner gate still applies after both.
The mint effect clears any existing code before anything else, so a code
minted for a previous account, a logout or an earlier screen can never
ride the Customize button into the new context. The client stores the
server's expiresAt and a click within thirty seconds of it takes the
fallback navigation and mints a replacement instead of opening a code
whose exchange would answer dead.
@feruzm
feruzm merged commit 5aaf3bd into develop Aug 12, 2026
12 checks passed
@feruzm
feruzm deleted the feature/hosting-handoff-exchange-code branch August 12, 2026 18:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Hosting: one-time exchange code for the signup session handoff

1 participant