BE-489, H-6417, H-6418: Kratos session refresh, passwordless password setup, Playwright overhaul#8638
BE-489, H-6417, H-6418: Kratos session refresh, passwordless password setup, Playwright overhaul#8638TimDiekmann wants to merge 14 commits intomainfrom
Conversation
Splits specs across four projects: `account` (signup, signin, password, MFA), `features` (feature regressions against a pre-seeded Alice via `storageState`), `guest` (unauth flows), and `extension` (browser plugin). `globalSetup` signs Alice in once and persists her session to `tests/.auth/alice.json` so feature tests skip the login flow. Supporting changes: - `shared/test-users.ts` centralises per-test dedicated users; each auth-mutating test gets its own identity to avoid cross-test leakage. - `shared/delete-user.ts` (new) idempotently removes leftover Kratos identities via the Graph admin endpoint before re-running signups. - `shared/signin-utils.ts` (new) offers a lean sign-in helper. - `shared/signup-utils.ts` now randomises shortnames and cleans up prior Kratos identities before registering. - `shared/runtime.ts` captures a small set of expected 4xx responses (whoami 401, AAL2-upgrade 422, recovery 422, self-service 400) as console-noise suppressions correlated by status code. - `shared/get-kratos-verification-code.ts` adds `getKratosRecoveryCode` and normalises Mailslurper timestamps to UTC. - `.env.test` allowlist lists the new test identities. - `.gitignore` excludes the per-test `.auth` storage-state directory. - Root `package.json` gains `start:test:backend` / `start:test:frontend` helpers to spin up only the services a given suite needs. - `docker-compose.yml` threads `SELFSERVICE_FLOWS_SETTINGS_PRIVILEGED_SESSION_MAX_AGE` through to Kratos with a 5-minute default so tests can tighten it via env var.
The popup runs several uncoordinated async loads on mount (`getUser` fires multiple times plus `useEntityTypes`), each independently writing a slice of state into `chrome.storage.local`. The previous suite interacted with the popup before those writes settled, so the backend-persisted `BrowserPluginSettings` entity leaked state between runs and races produced spurious duplicate chips. Rather than fight each race individually, this change: - Adds `waitForPopupStateLoaded` — resolves once `chrome.storage.local` has been idle for 500 ms, which empirically covers the mount-time fetches under CI latency. - Replaces UI-driven state clears (chip-by-chip delete button clicks) with direct `chrome.storage.local.set` writes in `resetOneOffState` / `resetAutomatedState`. `useStorageSync` picks up the change and re-renders the UI empty, and we sidestep the per-click debounced backend save that would otherwise collide with late `getUser` writes. - Replaces magic-number `sleep()` calls with deterministic waits: `waitForSettingsSave` listens for the next debounced `updateEntity` mutation; `selectEntityTypeOption` waits for the resulting chip. - Adds an `ADD ANOTHER` fallback for the Automated tab because `SelectScope`'s `showTable` / `draftRule` are `useState`-initialised from `anyTypesSelected` and therefore sticky across storage clears. - Uses a native `input` event dispatch for the quick-note textarea; `page.fill` silently skipped MUI's `onChange` when the stored value matched the new one. The plugin fixture now still clears local storage on start so the popup hydrates via `getUser()` rather than stale cache. Backend state cleanup moves into the per-test reset helpers. Plugin-browser: `infer-entities` no longer opens a doomed WebSocket when the user isn't logged in. A background-script `init` with no session cookies defers until a user action triggers `getWebSocket()`, and `reconnectWebSocket` short-circuits the same way, which prevents the server from closing each connection after its 5-second unauthenticated timeout. Supporting: added `@types/chrome`, included `global-setup.ts` in `tsconfig.json`, moved the extension fixture to `shared/browser-plugin-fixtures/` to match the new `extension/` project layout.
The singleton `apolloClient` exported from `create-apollo-client.ts` is shared across SSR requests; Apollo's query deduplication uses `(query, variables)` as the dedup key and ignores `context`, so concurrent SSR requests for the same query share a response. Disable dedup on the server-side client; browser clients keep it. Also expands the `@todo` on the SSR `meQuery` prefetch in `_app.page.tsx` so the interaction is visible to whoever addresses the caching todo.
Extracts the two-step disable flow (unlink TOTP, then clear backup codes) into `executeDisableTotp`. Kratos keeps enforcing AAL2 as long as any second factor remains, so the follow-up `lookup_secret_disable` is load-bearing — without it the user would still be prompted for an authenticator code they no longer have at next sign-in. Also drops `currentPassword`, `currentPasswordError`, and `isRecoveryFlow` state: password changes no longer accept a current password in the UI (Kratos enforces privileged-session age via `privileged_session_max_age`), and the recovery-flow marker is unused.
- Delete `loginUsingTempForm`, `loginUsingUi`, and `getDerivedPayloadFromMostRecentEmail`; consolidate on `signInWithPassword`. - Rename `signOut` → `clearSessionCookies`. - Deduplicate `defaultPassword`; make `password` required in signup helpers. - `callGraphQlApi` checks HTTP status and GraphQL errors. - `global-setup` try/finally to avoid leaking Chromium. - `decodeBase32` throws on invalid characters. - `openPopupTab` helper pins `popupTab` in storage before clicking. - Tighten chip assertions to cover linked types. - `submitSettingsUpdate` surfaces errors instead of re-rejecting. - Null-guard `extractBackupCodesFromFlow` textContent. - Show error on empty backup code regeneration. - Correlate `gatherUiNodeValuesFromFlow` / `useKratosErrorHandler` generics with flow type. - Derive `flowMetadata.settingsWithPassword` from `settings`. - Log malformed Kratos redirect URLs. - `waitForConnection` timeout + CLOSED/CLOSING check. - Wrap WebSocket `JSON.parse` in try/catch. - Catch `getCookieString` rejection in setInterval. - Log WebSocket error event.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
3 Skipped Deployments
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #8638 +/- ##
==========================================
- Coverage 62.49% 62.49% -0.01%
==========================================
Files 1318 1318
Lines 134234 134235 +1
Branches 5520 5521 +1
==========================================
Hits 83893 83893
- Misses 49426 49427 +1
Partials 915 915
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
- security.page.tsx: remove unnecessary optional chains flagged by @typescript-eslint/no-unnecessary-condition. - infer-entities.ts: sanitize event.data with full control-char strip + 1 000-char cap (CodeQL log-injection finding). - changeSidebarListDisplay: wait for the sidebar to reflect the new display mode after toggling (uppercase "ENTITIES" / "TYPES" header for list mode). Fixes a race in CI where the test clicked "Entities" before the sidebar had updated from link to list mode.
The backend-integration test `user.test.ts` creates an incomplete user with `charlie@example.com`. The allowlist was rewritten in the test reorganisation commit but omitted this email, causing the shortname-update test to fail with FORBIDDEN.
- Scope sidebar assertions to `page-sidebar` testid to avoid matching
settings-page labels (e.g. "Entities as a")
- Use `getByText(section, { exact: true })` for list mode,
`getByRole("link")` for link mode instead of case-insensitive `text=`
locator
- Remove `sleep(5_000)` and unused import from entity-type-creation test
- Wait for create-entity-type button visibility before clicking
- Remove event.data from WebSocket error log (CodeQL log injection +
ESLint no-control-regex
- Wait for updateEntity GraphQL response after toggling sidebar preference to prevent concurrent-update errors (FE-600) - Add expandSidebarSection helper that checks MuiCollapse state before clicking, avoiding double-toggle - Sign in bob during globalSetup so entity-type-creation runs as bob while entities-page runs as alice — no shared entity conflicts - Remove withTestUser wrapper, callers use createUserAndCompleteSignup directly - Remove event.data from WebSocket error log (CodeQL + ESLint)
PR SummaryHigh Risk Overview Server-side Apollo is hardened by disabling query deduplication for SSR to prevent cross-request auth cookie leakage, and Kratos flow utilities/error handling are tightened (safer redirect parsing/logging, stronger TypeScript typing). Playwright is restructured into flow-based projects with global auth state seeding, dedicated test users, expanded account coverage (signin/signout/password/recovery/MFA), and more robust extension tests; additional stability fixes land in the browser plugin WebSocket logic (cookie guarding, timeouts, parse/error handling). Reviewed by Cursor Bugbot for commit dc4c82e. Bugbot is set up for automated code reviews on this repo. Configure here. |
🤖 Augment PR SummarySummary: This PR updates HASH’s authentication flows and test infrastructure to better rely on Ory Kratos for re-authentication and to improve integration test stability. Changes:
Technical Notes: Several new flows depend on Kratos returning 🤖 Was this summary useful? React with 👍 or 👎 |
- Rename getSessionCookies/hasSessionCookies/getCookieString to getApiOriginUrl/isLoggedIn/buildWebsocketCookieString - isLoggedIn only checks for ory_kratos_session cookie instead of requiring both CSRF and session (CSRF is only needed for websocket) - Extract shared getApiOriginUrl helper
- Guard against non-SettingsFlow response body in security.page.tsx error handler with optional chaining - Wrap recovery code polling in try/catch to survive transient fetch errors, matching the verification code helper - Create tests/.auth/ directory in globalSetup for clean checkouts
Both getKratosVerificationCode and getKratosRecoveryCode duplicated the same mailslurper polling, timestamp filtering, and retry logic. The recovery variant was missing the diagnostic summary on failure. Extract a parameterized pollForKratosCode that takes subject filter, code extractor, and email type label. Both callers are now thin wrappers with equal diagnostics on timeout.
mustGetCsrfTokenFromFlow was called inline as an argument to submitSettingsUpdate. A synchronous throw would skip the Promise chain and its .finally() cleanup, leaving updatingPassword stuck at true. Extract the token before starting the async chain.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit dc4c82e. Configure here.
Benchmark results
|
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| resolve_policies_for_actor | user: empty, selectivity: high, policies: 2002 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: medium, policies: 1001 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: high, policies: 3314 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: medium, policies: 1526 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: high, policies: 2078 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: medium, policies: 1033 | Flame Graph |
policy_resolution_medium
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| resolve_policies_for_actor | user: empty, selectivity: high, policies: 102 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: medium, policies: 51 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: high, policies: 269 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: medium, policies: 107 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: high, policies: 133 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: medium, policies: 63 | Flame Graph |
policy_resolution_none
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| resolve_policies_for_actor | user: empty, selectivity: high, policies: 2 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: medium, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: high, policies: 8 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: medium, policies: 3 | Flame Graph |
policy_resolution_small
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| resolve_policies_for_actor | user: empty, selectivity: high, policies: 52 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: medium, policies: 25 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: high, policies: 94 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: medium, policies: 26 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: high, policies: 66 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: medium, policies: 29 | Flame Graph |
read_scaling_complete
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| entity_by_id;one_depth | 1 entities | Flame Graph | |
| entity_by_id;one_depth | 10 entities | Flame Graph | |
| entity_by_id;one_depth | 25 entities | Flame Graph | |
| entity_by_id;one_depth | 5 entities | Flame Graph | |
| entity_by_id;one_depth | 50 entities | Flame Graph | |
| entity_by_id;two_depth | 1 entities | Flame Graph | |
| entity_by_id;two_depth | 10 entities | Flame Graph | |
| entity_by_id;two_depth | 25 entities | Flame Graph | |
| entity_by_id;two_depth | 5 entities | Flame Graph | |
| entity_by_id;two_depth | 50 entities | Flame Graph | |
| entity_by_id;zero_depth | 1 entities | Flame Graph | |
| entity_by_id;zero_depth | 10 entities | Flame Graph | |
| entity_by_id;zero_depth | 25 entities | Flame Graph | |
| entity_by_id;zero_depth | 5 entities | Flame Graph | |
| entity_by_id;zero_depth | 50 entities | Flame Graph |
read_scaling_linkless
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| entity_by_id | 1 entities | Flame Graph | |
| entity_by_id | 10 entities | Flame Graph | |
| entity_by_id | 100 entities | Flame Graph | |
| entity_by_id | 1000 entities | Flame Graph | |
| entity_by_id | 10000 entities | Flame Graph |
representative_read_entity
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/block/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/book/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/building/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/organization/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/page/v/2
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/person/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/playlist/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/song/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/uk-address/v/1
|
Flame Graph |
representative_read_entity_type
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| get_entity_type_by_id | Account ID: bf5a9ef5-dc3b-43cf-a291-6210c0321eba
|
Flame Graph |
representative_read_multiple_entities
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| entity_by_property | traversal_paths=0 | 0 | |
| entity_by_property | traversal_paths=255 | 1,resolve_depths=inherit:1;values:255;properties:255;links:127;link_dests:126;type:true | |
| entity_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:0;links:0;link_dests:0;type:false | |
| entity_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:0;links:1;link_dests:0;type:true | |
| entity_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:2;links:1;link_dests:0;type:true | |
| entity_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:2;properties:2;links:1;link_dests:0;type:true | |
| link_by_source_by_property | traversal_paths=0 | 0 | |
| link_by_source_by_property | traversal_paths=255 | 1,resolve_depths=inherit:1;values:255;properties:255;links:127;link_dests:126;type:true | |
| link_by_source_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:0;links:0;link_dests:0;type:false | |
| link_by_source_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:0;links:1;link_dests:0;type:true | |
| link_by_source_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:2;links:1;link_dests:0;type:true | |
| link_by_source_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:2;properties:2;links:1;link_dests:0;type:true |
scenarios
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| full_test | query-limited | Flame Graph | |
| full_test | query-unlimited | Flame Graph | |
| linked_queries | query-limited | Flame Graph | |
| linked_queries | query-unlimited | Flame Graph |

🌟 What is the purpose of this PR?
Three related auth improvements plus a comprehensive Playwright test suite overhaul that grew out of the H-6219 TOTP restoration work:
privileged_session_max_agefor re-authentication instead of a custom current-password prompt.🔗 Related links
🚫 Blocked by
Nothing.
🔍 What does this change?
Test suite reorganisation (
ac943f87cd)account/(signup, signin, password, MFA),features/(pre-authenticated viastorageState),guest/, andextension/.globalSetupsigns Alice in once and persists her session.shared/test-users.ts+shared/delete-user.ts.flows × browserswith chromium/firefox/webkit.test:integrationdefaults to*-chromium; firefox/webkit available via dedicated scripts.Browser-extension test stabilisation (
7f900e3c6f)waitForPopupStateLoaded— 500 ms idle window onchrome.storage.onChangedso mount-time fetches settle before interaction.resetOneOffState/resetAutomatedState— directchrome.storage.local.setwrites instead of per-chip UI clicks (avoids debounce races with lategetUserwrites).openPopupTab— pinspopupTabin storage before clicking tab to prevent lategetUserfrom flipping it back.selectEntityTypeOption— ArrowDown + Enter with chip-count assertion.infer-entities.tsno longer opens a doomed WebSocket when no session cookies exist.Apollo SSR fix (
47d53ffa15)queryDeduplicationon the server-side Apollo client singleton; the dedup key ignorescontext(auth cookies) so concurrent SSR requests for the same query would share one response.TOTP disable consolidation (
1bc1fdba6c)executeDisableTotp(unlink TOTP + clear backup codes). DropcurrentPassword/isRecoveryFlowstate — Kratos enforces privileged session age viaprivileged_session_max_age.Review findings cleanup (
fb72744484)loginUsingTempForm,loginUsingUi,getDerivedPayloadFromMostRecentEmail.signOut→clearSessionCookies.submitSettingsUpdatecatches all errors instead of re-rejecting.gatherUiNodeValuesFromFlow/useKratosErrorHandlergenerics tightened.flowMetadata.settingsWithPasswordderived fromsettings.callGraphQlApichecks HTTP status + GraphQL errors.waitForConnectiontimeout + CLOSED/CLOSING check.JSON.parsewrapped, error handler logs event,getCookieStringrejection caught.decodeBase32throws on invalid characters.global-setuptry/finally.Pre-Merge Checklist 🚀
🚢 Has this modified a publishable library?
This PR:
📜 Does this require a change to the docs?
The changes in this PR:
🕸️ Does this require a change to the Turbo Graph?
The changes in this PR:
waitForPopupStateLoaded) because the popup fires multiple uncoordinated async fetches on mount. Stable in 10/10 consecutive runs locally but the heuristic is not a guarantee under CI load.🐾 Next steps
pdfjs-distas explicit frontend dependency (FE-595)import/no-extraneous-dependenciesglobally in eslint base config (FE-596)security.page.tsx14×useStateintouseReducer🛡 What tests cover this?
❓ How to test this?
yarn start:testcd tests/hash-playwright && npx playwright test --project '*-chromium'npx playwright test --project '*-firefox'(requiresnpx playwright install firefox)