Commit 931c8d8
authored
fix: never render the cloud sign-up email form before region detection settles (Comfy-Org#14945)
## Summary
The cloud sign-up email form was live and submittable for up to two
seconds before the region check finished, so a user the China rule is
meant to block could create the account inside that window.
## Changes
- **What**: `userIsInChina` was a `ref(false)` resolved in a mount hook,
so `false` meant both "still checking" and "not in China" and the form
rendered immediately. Region state is now a three-way `pending | blocked
| allowed` behind `useRegionGate()`, with a skeleton holding the space
until detection settles. The form cannot render early because it sits
behind `v-else`.
`isInChina()` now prefers Cloudflare's edge geo-IP over the reachability
heuristic. The heuristic was unsound in both directions — a VPN user in
China reaches Google and is waved through, a `zh-CN` user anywhere is
blocked whenever Google is briefly unreachable — and it pinged
google.com and baidu.com with the user's IP from the page whose purpose
is minimizing data exposure. It stays as the fallback when the edge
cannot answer, so the desktop mirror picker keeps its current tolerance.
Every probe leg is now bounded; two previously had no deadline at all
and could hang until the browser's own network timeout. Detection always
settles, so callers need no timeout of their own.
`SignInContent.vue` had the identical construction and now consumes the
same composable, so the race is closed on both sign-up surfaces rather
than one.
- **Breaking**: None. Email sign-up stays blocked in China, Google and
GitHub stay allowed, login stays unguarded, and the user-facing copy is
byte-identical.
## Review Focus
The policy shape is unchanged; only the timing of the gate is.
Previously the form was interactive during detection, which under a
data-controllership rule is a real hole rather than a cosmetic flash.
Two constants carry over from the old code rather than being retuned:
`PROBE_TIMEOUT_MS` (2000, previously the Google leg's race) and
`CHINA_LATENCY_MS` (150). No new timeout was introduced, and the
composable and both views contain no timers — a caller-side deadline
would decide "allowed" while a real "blocked" answer was still in
flight.
`getClientCountry()` reads `loc=` from `/cdn-cgi/trace`. Cloudflare
documents that path only as a troubleshooting aid, not a supported API,
so the durable form is a first-party endpoint echoing `CF-IPCountry`.
Failure returns `undefined` and falls through to the heuristic.
A client-side gate is bypassable by design. See the comment below on
where enforcement has to live.
## Screenshots
Simulated China conditions, same moments on each branch, after choosing
**Use email instead**.
**Before** — the form is live throughout detection.
300 ms
<img
src="https://github.com/user-attachments/assets/a829cf19-663f-4d32-9058-2f1d92177df6"
width="640">
1200 ms
<img
src="https://github.com/user-attachments/assets/cb2de729-3d10-4df2-82d8-c46d00af21d8"
width="640">
2800 ms — the notice finally replaces it
<img
src="https://github.com/user-attachments/assets/9f87673a-4934-423d-8fe7-91ca73399236"
width="640">
**After** — a skeleton holds the space; no email or password field
exists in the DOM.
300 ms
<img
src="https://github.com/user-attachments/assets/b0b6d743-d08d-4a56-91de-c8717e6fb257"
width="640">
1200 ms
<img
src="https://github.com/user-attachments/assets/ae73b761-f360-4f1f-81a2-e25040b6e5b2"
width="640">
2800 ms — settled to the notice
<img
src="https://github.com/user-attachments/assets/26ed1bf0-9753-4a50-bc49-a2cd82003e05"
width="640">
## Testing
44 tests across five files, every one mutation-checked. **43 killed, 1
survivor** (recorded below rather than papered over).
`isInChina` / `getClientCountry` — 17 cases, 100% branch coverage:
| Case | Mutation applied | Result |
| --- | --- | --- |
| Uppercases the `loc` value | drop `.toUpperCase()` | fails |
| `undefined` for non-200 | delete the `response.ok` guard | fails |
| `undefined` for a body with no `loc` | default the missing line to a
country | fails |
| `undefined` for an empty `loc` | `\|\| undefined` → `?? undefined` |
fails |
| `undefined` for `XX` / `T1` sentinels | return the country unfiltered;
trim the sentinel set | 2 fail |
| `undefined` for a network error | rethrow instead of catching | fails
|
| Edge never answers → gives up | bare `fetch`; deadline never rejects;
timeout 2000→4000 | fails |
| Prefers the edge over the heuristic | run the probe eagerly alongside
the edge | fails |
| Edge `CN` → blocked / other → allowed | `country === 'CN'` → `!==` | 2
fail |
| Falls back to the heuristic on a sentinel | remove the sentinel filter
| 2 fail |
| Edge down + Google reachable → allowed | invert the google-success
return | fails |
| Edge down + Baidu fast → blocked | latency compare → `false`;
threshold 150→0 | fails |
| Neither probe answers → locale decides | inner catch → `true`; force
the locale check | fails |
| Every probe hangs → still settles | unbound a probe leg | fails |
Region gate and both sign-up surfaces — 27 cases:
| Case | Mutation applied | Result |
| --- | --- | --- |
| Starts pending | `ref('pending')` → `ref('allowed')` | fails |
| Pending → blocked / → allowed | invert the two status branches | 2
fail |
| Rejection fails open to allowed | remove `.catch`; `.catch(() =>
true)` | fails |
| A slow real answer is never pre-empted | add a caller-side 3s deadline
| fails |
| Form withheld while pending (both surfaces) | pending branch →
`v-if="false"` | 2 fail |
| Notice replaces form in China (both) | blocked branch →
`v-else-if="false"` | 2 fail |
| Form renders outside China (both) | invert the blocked branch | 2 fail
|
| Form released when detection fails (both) | invert the blocked branch
| 2 fail |
| Never renders the form in China, pending or settled | disable both
branches | fails |
| Social sign-up unaffected in every state (4 rows) | gate the social
buttons on `regionStatus === 'allowed'` | 4 fail |
| Login view has no region gate | `CloudSignInForm` → `v-if="false"` |
fails |
| Query forwarding, free-runs copy, email toggle, webview notice | drop
`query: route.query`; alter the count; force the branch | 4 fail |
**One survivor, left in place.** `SignInContent > leaves sign-in ungated
by region` is a real assertion — deleting `SignInForm` kills it, and it
catches a gate that withholds sign-in *while the probe is pending*. What
it cannot catch is a gate that revokes sign-in once the probe settles to
`blocked`, which is what its name promises. Cause is timing: the
assertion resolves on the first tick, before `onMounted`'s gate flips.
Its `CloudLoginView` twin is genuinely killed, because that view has no
gate wired at all.
Also worth recording: renaming the locale key `signupBlocked` breaks no
test, because `t`/`st` are mocked to echo the key. The two
`signup_blocked` tests still pin real branch routing (killed three
ways), but locale-key existence is not covered on this branch.
`auth/popup-blocked` is covered, via the locale-derived table in Comfy-Org#14944.
The `SignInContent` dialog is covered by unit assertions rather than a
screenshot — driving it needs a local ComfyUI backend.1 parent f51e309 commit 931c8d8
9 files changed
Lines changed: 755 additions & 50 deletions
File tree
- src
- components/dialog/content
- composables/auth
- platform/cloud/onboarding
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
7 | | - | |
8 | 7 | | |
9 | 8 | | |
10 | 9 | | |
11 | 10 | | |
12 | 11 | | |
13 | 12 | | |
14 | 13 | | |
15 | | - | |
16 | | - | |
17 | | - | |
18 | | - | |
| 14 | + | |
| 15 | + | |
19 | 16 | | |
20 | | - | |
21 | | - | |
22 | | - | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
23 | 61 | | |
24 | 62 | | |
25 | | - | |
26 | | - | |
27 | | - | |
28 | | - | |
29 | | - | |
30 | | - | |
31 | | - | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
32 | 66 | | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
33 | 71 | | |
34 | | - | |
35 | | - | |
36 | | - | |
37 | | - | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
38 | 81 | | |
39 | | - | |
40 | | - | |
41 | | - | |
42 | | - | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
43 | 102 | | |
44 | | - | |
45 | 103 | | |
46 | 104 | | |
47 | | - | |
48 | | - | |
49 | | - | |
50 | | - | |
51 | | - | |
52 | | - | |
53 | | - | |
54 | | - | |
| 105 | + | |
| 106 | + | |
55 | 107 | | |
56 | | - | |
57 | 108 | | |
58 | 109 | | |
59 | 110 | | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
60 | 120 | | |
61 | | - | |
| 121 | + | |
62 | 122 | | |
0 commit comments