v1.9.0: SuperTokens client login + refresh (and the bug that made every SuperTokens login impossible) - #13
Merged
Conversation
init.js handed the provider list to ThirdParty.init as `signInUpFeature`. The SDK reads `signInAndUpFeature`. The key is optional in its TypeInput and JavaScript has no excess-property check at runtime, so the wrong spelling was dropped in silence - no throw, no warning, no log. The result: a stack that initialises cleanly, passes every existing test, logs `providers=github,discord` (it logs what buildProviders returned, not what the recipe received), and then answers every /auth/authorisationurl with 400 "the provider github could not be found in the configuration". That is what production returns today on v1.8.4, for both providers. v1.8 tested both halves and never the seam: buildProviders() returns the right list, init refuses to start when the list is empty, and nothing asserted the list reaches the recipe. buildProviders' own empty-list guard cannot catch this - the list is non-empty, it just never arrives. `dual` hid it entirely, because the client still logs in through passport and nothing reached the endpoint. The new test runs the config init.js actually passes through the SDK's own normaliser, so it fails on a wrong key and on a future SDK rename alike, rather than merely restating the fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The client half of Phase 5. Three hand-rolled fetch calls rather than supertokens-web-js, whose signOut() targets the /auth/signout v1.8 removed on purpose - it clears only the SuperTokens half of a dual-stack session. GET /api/auth-info (public) is how the client learns which stack to drive. It cannot live on /api/config as the plan proposed: that route is behind requireAuth and the caller is by definition not logged in yet. `loginFlow` is the server's decision rather than something the client re-derives, so the policy lives in one place - in `dual` it says `supertokens`, because exercising that path is the entire point of dual. Both stacks work from one bundle. Hardcoding SuperTokens would break `passport`, which is still the default and the documented rollback. Session refresh serialises through one shared promise. A refresh per 401 would send N concurrent calls presenting the SAME refresh token; SuperTokens rotates it on use, so calls 2..N look like token theft to the core and it revokes the session - a routine renewal becoming a forced logout, and only under concurrency. Same shape as server/userLock.js. Two of the new tests were confirmed by mutation rather than by inspection: deleting the single-flight guard makes the concurrency test report 3 refreshes instead of 1, and disabling the client's callback leg fails two smoke checks. The first version of the concurrency test passed with the guard deleted (its stub keyed "already expired" off the URL, so only one of the three requests ever got a 401) - the comment in the test records that so it cannot regress. 627 -> 696 vitest (SQLite), 39 -> 49 smoke. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The docs said `supertokens` mode was unusable because the client had no login flow. That is no longer the reason it should be approached carefully, so the five places that said so - README, .env.example, unraid-template.xml, the rollout runbook's preamble and D6, and authentication-methods.md Phase 5 - now say what is actually true. They do not say it is proven. The provider-registration fix IS verified against a real SuperTokens core 12: booted in dual, the request production answers with 400 returns 200 and a valid GitHub authorize URL, and restoring the typo reproduces production's error byte for byte. But POST /auth/signinup and the identity mapping need a real OAuth round trip, so they are still stubbed everywhere - and the mapping is the part that decides whether a returning player lands on their own save or an empty one. D6 and Phase 5 now carry the four-step gate to run on a real deployment before setting AUTH_MODE=supertokens. Worth keeping from the real-core run: GET /api/auth-info reported providers:["github","discord"] with the typo in place and without it. The boot-side view is identical either way, which is why a whole release shipped with no SuperTokens login possible and nothing anywhere said so. 696 vitest (SQLite), 722 (Postgres), 49 smoke. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
NeverEndingCode
force-pushed
the
worktree-v1.9-supertokens-client
branch
from
August 8, 2026 16:14
8e08137 to
37a61dc
Compare
NeverEndingCode
marked this pull request as ready for review
August 8, 2026 16:14
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase 5 of the SuperTokens migration: the client can now log in through SuperTokens and refresh its own session. Plan:
docs/superpowers/plans/2026-08-08-v1.9-supertokens-client.md.The headline is a bug, not a feature
Task 1 of the plan was "drive the login by hand against production
dual, no code". It failed immediately, for both providers:server/supertokens/init.jspassed the OAuth providers toThirdParty.initassignInUpFeature. The SDK readssignInAndUpFeature. That key is optional in itsTypeInputand JavaScript does not reject unknown properties, so the provider list was discarded silently — no throw, no warning, no log. No SuperTokens login was possible on any v1.8 build.Verified causally against a real SuperTokens core 12, same request, same core, both ways:
init.jssayssignInUpFeature(v1.8)400 {"message":"the provider github could not be found in the configuration"}— byte-for-byte productionsignInAndUpFeature(v1.9)200 {"status":"OK","urlWithQueryParams":"https://github.com/login/oauth/authorize?..."}Why a whole release hid it:
providers=github,discordin both runs — it logs whatbuildProviders()returned, not what the recipe received.buildProviders()returns the right list, and that init refuses to start when the list is empty. Nothing asserted the list arrives.buildProviders' own "no OAuth provider configured" guard structurally cannot catch it — the list is non-empty, it just never lands.dualnever exercised it, because the client logged in via passport. A quietdualdeployment was not evidence the path worked; it could not have been.Same shape as the
superTokensUserIdcapital-T bug from v1.8. The new test runs the configinit.jsactually passes through the SDK's own normaliser, so it fails on a wrong key and on a future SDK rename, rather than restating the fix.The client work
client/src/game/auth.js— the three-call flow, hand-rolled rather thansupertokens-web-js, whosesignOut()targets the/auth/signoutv1.8 removed for clearing only half of a dual-stack session.GET /api/auth-info(public) — tells the client which stack to drive. It could not go on/api/configas the plan proposed: that route is behindrequireAuthand the caller is by definition not logged in yet. One build therefore servespassportandsupertokensalike, so the documented rollback keeps working.Testing
696 vitest (SQLite) / 722 (Postgres) / 49 smoke — up from 662 / 688 / 39.
Two tests were confirmed by mutation rather than inspection:
tests/e2e/smoke-v19.mjsdrives the whole redirect round trip over the built client by pointing the stubbed authorisation URL back at the app's own callback, so no external service and no core are needed.What this does NOT prove
POST /auth/signinupand the identity mapping have still never run against a real core — that needs a real OAuth round trip, so every automated test stubs it. Since the mapping decides whether a returning player lands on their own save or an empty one, do not skip toAUTH_MODE=supertokens.Plan Task 1 Steps 2–3 and Task 4 Steps 1–2 remain open and are the operator's, on a deployed build. The four-step gate is written up in
docs/authentication-methods.mdPhase 5 and runbook D6:signinupmust returnuser.idequal to the existingusers.id, not a UUID.identities.supertokens_user_idmust hold a real SuperTokens id, not our own.shadow:checkstill 6/6 with no new identities.Docs in five places that called
supertokensmode unusable now say what is actually true, without claiming it is proven.Order of operations
This was opened as a draft "until the gate has been run", which was circular — the gate needs a build carrying the fix, and that build only exists once this is merged and tagged. The gate gates
AUTH_MODE=supertokens, not this merge.main(never the branch) — the tag push is what publishes to GHCR. Without it the Unraid box cannot get the fix.AUTH_MODE=dual; nothing about logins changes yet, becausedualkeeps accepting existing sessions either way.AUTH_MODE=supertokens.Merging is safe ahead of the gate:
dualremains the resting state, legacy 90-day JWT cookies stay valid throughout, and rollback is stillAUTH_MODE=passport+ restart.🤖 Generated with Claude Code