`syncAccount()` derived the current colour from `window.location.host`, so on the server it returned the url untouched. Every server-rendered cross-colour link therefore shipped without the `sync-account` marker, and since that marker is the only trigger for the destination's bootstrap (useDomainSync -> /api/auth/authorize), a browser arriving at the other colour without an existing session stayed signed out. Confirmed in production, not just locally. It is masked in normal use because the destination's session cookie is 30-day rolling, so people are usually already signed in there — you only see it in a profile that has never signed in on that colour. Extracts a pure `syncAccountFor(url, colour, domains)` and adds a `useSyncAccount()` hook that reads the colour from AppProvider context. The colour deliberately does NOT go in a module-scope global beside `serverDomains`: one Next process serves every colour concurrently, so a per-request value there would leak across requests, and nothing in lint catches that. Converts the gated mature-content link, which is the path this was found on. ~31 other call sites still use the bare `syncAccount()`; any that render on the server have the same bug, and each needs checking individually rather than a mechanical sweep. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
7.5 KiB
syncAccount Utility Migration
SUPERSEDED (2026-08). The 12-site migration below landed; two of the files it names (
MatureContentMigrationAlert.tsx,SensitiveShield.tsx) no longer exist. Two things it says are now wrong: there is noredirectUrlsecond arg /sync-redirectparam, and the destination does not pull a token from/api/auth/sync(deleted with the swap bridge) — it initiates the OAuth auth-code flow at/api/auth/authorize. AndsyncAccount(url)is no longer the API to reach for: it derives the current colour fromwindow.location.host, so it silently no-ops during SSR and every server-rendered cross-colour link shipped without the marker. UseuseSyncAccount()(src/hooks/useSyncAccount.ts) in anything that renders on the server;syncAccountFor(url, colour, domains)is the pure form. Kept for the bucket rationale only.
Background
The sync-account query parameter signals to the destination domain which color
domain holds the user's session, so it can pull the token via
/api/auth/sync (see useDomainSync).
Until now, every cross-domain link constructed this param by hand, with the
color value either:
- Hardcoded to
green(correct for .com → .red flows, no-op for .red → .com flows since the source matches the destination) - Hardcoded to
blue(legacy from when blue was the primary domain slot —useDomainSyncresolvesserverDomains.bluewhich may or may not matchserverDomains.redper env config) - Hardcoded to
yellowin one site — invalid as aColorDomain, silently bails insideuseDomainSync
The new syncAccount(url, redirectUrl?) utility resolves both
the current host and the URL's host against the configured serverDomains map
and emits the correct source color automatically. The optional second arg
attaches a sync-redirect param so the destination can navigate to a clean
path after the auth swap. Same-color or relative URLs get the URL back unchanged.
Scope
Migrate every call site that hand-rolls sync-account=... to use
syncAccount(url). Bundle as one PR.
Sites to migrate
Bucket 1 — Same-value migrations (no behavior change)
User is on .com → linking to .red, hardcoded ?sync-account=green. The
new utility emits green from .com. Pure refactor.
| File | Line | Change |
|---|---|---|
| MatureContentMigrationAlert.tsx | 63-64 | Wrap `//${redDomain}` and the 'https://civitai.red' fallback with syncAccount(...); drop hardcoded ?sync-account=green |
| YellowBuzzMigrationNotice.tsx | 51-55 | Replace syncParams + redUrl with syncAccount(`//${redDomain ?? 'civitai.red'}/`, '/user/buzz-dashboard'). Only site using sync-redirect — pass the post-sync path as the second arg. |
| SensitiveShield.tsx | 64 | Replace the manual separator + sync-account=green concat with syncAccount(\//${redDomain}${router.asPath}`)` |
Bucket 2 — Bug fixes (current value is a no-op)
Hardcoded sync-account=green while linking to the green domain. Inside
useDomainSync, host === syncDomain short-circuits and the param does
nothing today. After migration, the source resolves to red (the user's
actual domain) and the auth swap actually happens.
| File | Line | Change |
|---|---|---|
| QueueItem.tsx | 744 | pricingHref = features.isGreen ? '/pricing' : syncAccount(\//${serverDomains.green}/pricing`)` |
| NoCryptoUpsell.tsx | 33-34 | greenBuzzUrl = syncAccount(\//${greenDomain}/purchase/buzz`); greenPricingUrl = syncAccount(`//${greenDomain}/pricing`)` |
Bucket 3 — sync-account=blue → current source color
Hardcoded sync-account=blue is legacy. Migrating shifts the value to the
user's current color — green from .com (which the same-host short-circuit
will then omit) or red from .red. The intent is unchanged: "use my current
session at the destination."
| File | Line | Change |
|---|---|---|
| buzz.utils.ts | 27-36 (useBuyBuzz) |
Drop 'sync-account': 'blue' from the query object; wrap the final URL passed to window.open with syncAccount(...) |
| pricing/index.tsx | 50-61 (auto-redirect effect) | Same pattern — drop the param from the query object, wrap the final URL with syncAccount(...) |
| YellowMembershipUnavailable.tsx | 10-13 | greenPricingUrl = syncAccount(\//${serverDomains.green}/pricing?${QS.stringify({ buzzType: 'green' })}`)` |
| BuzzPurchaseImproved.tsx | 360-373 | Drop 'sync-account': 'blue' from query; wrap window.open URL with syncAccount(...) |
| GreenEnvironmentRedirect.tsx | 41-49 (handleManualRedirect) |
Drop 'sync-account': 'blue' from query; wrap window.location.href value with syncAccount(...) |
| MembershipUpsell.tsx | 107 (Become a member) | Replace the ?sync-account=blue concat with syncAccount(pricingUrl) |
Bucket 4 — Bug fix in pages/user/membership.tsx
handleRedirectToOtherEnvironment has two latent bugs:
- Emits
sync-account=yellowwhen going from .red → .com.'yellow'is not aColorDomain, souseDomainSyncsilently bails — the auth swap never happens. - Uses
serverDomains.bluefor the .red destination, which is the legacy blue-means-red convention. Aligning with the .com→green / .red→red mapping meansserverDomains.red.
| File | Line | Change |
|---|---|---|
| pages/user/membership.tsx | 132-141 | Replace function body with const targetDomain = otherBuzzType === 'green' ? serverDomains.green : serverDomains.red; window.open(syncAccount(\//${targetDomain}/user/membership`), '_blank', 'noreferrer');` |
Validation note: the serverDomains.blue → serverDomains.red swap
assumes both keys point to the same host in production. If the env config
has them as distinct hosts, this changes the destination, not just the
sync param. Worth eyeballing the env vars before merging.
Sites NOT to migrate
| File | Line | Reason |
|---|---|---|
| LoginContent.tsx | 53 | The ?sync-account=green is on a return URL whose host equals the current host — syncAccount would short-circuit and drop the param. Intent here is "after authenticating on green, sync from green," which is the post-auth source, not the user's current domain. The utility can't express that. |
| pages/purchase/buzz.tsx | 18 | Server-side check that reads the param, doesn't write it. |
| useDomainSync.tsx | — | The consumer of the param. |
Acceptance
- All 12 sites in Buckets 1–4 use
syncAccount(...). - No remaining
sync-account=green,sync-account=blue, orsync-account=yellowliterals (except theLoginContent.tsxexemption). pnpm run typecheckpasses.- Manual smoke: clicking a .com → .red link from .com still appends
?sync-account=green; clicking the same-domain link no longer appends a param.