From fb4d11435c5616b79fc07a4eddf02a975677a674 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 7 Sep 2026 09:55:23 +0900 Subject: [PATCH 1/7] test(auth): reproduce unsafe sign-in recovery states --- frontend/src/App.test.tsx | 27 +++++++++++++++++++++++++++ frontend/src/oidcReturnUrl.test.ts | 23 +++++++++++++++++++++-- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index 790c4da69..8dbea2c20 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -74,6 +74,33 @@ it("announces a lazy surface load failure with a recovery action", () => { describe("App, unauthenticated", () => { + it("offers a safe sign-in retry and retains the pre-callback destination", async () => { + const privateError = "synthetic-private-auth-response"; + window.history.replaceState({}, "", `/?error=access_denied&error_description=${privateError}&state=stale`); + window.sessionStorage.setItem(OIDC_RETURN_URL_STORAGE_KEY, "/?post=remembered#evidence"); + mockAuth = { ...mockAuth, error: new Error(privateError) }; + + render(); + + expect(screen.getByRole("alert")).toHaveTextContent("This request failed. Retry the same action."); + expect(screen.queryByText(privateError)).not.toBeInTheDocument(); + await userEvent.click(screen.getByRole("button", { name: "Log in" })); + expect(signinRedirect).toHaveBeenCalledWith({ state: { returnUrl: "/?post=remembered#evidence" } }); + expect(window.localStorage.getItem(OIDC_RETURN_URL_STORAGE_KEY)).toBe("/?post=remembered#evidence"); + }); + + it("offers sign-in recovery when an authenticated session has no access token", async () => { + window.history.replaceState({}, "", "/?post=abc&error=access_denied#evidence"); + mockAuth = { ...mockAuth, isAuthenticated: true }; + + render(); + + expect(screen.getByRole("alert")).toHaveTextContent("This request failed. Retry the same action."); + expect(screen.queryByText(/access token/i)).not.toBeInTheDocument(); + await userEvent.click(screen.getByRole("button", { name: "Log in" })); + expect(signinRedirect).toHaveBeenCalledWith({ state: { returnUrl: "/?post=abc#evidence" } }); + }); + it("shows a login button that starts the real OIDC redirect", async () => { window.history.replaceState({}, "", "/?post=abc#evidence"); render(); diff --git a/frontend/src/oidcReturnUrl.test.ts b/frontend/src/oidcReturnUrl.test.ts index 2ecddb1c1..1a40fea12 100644 --- a/frontend/src/oidcReturnUrl.test.ts +++ b/frontend/src/oidcReturnUrl.test.ts @@ -24,12 +24,28 @@ describe("OIDC return URL handling", () => { // one-time code/state; a return URL built from it must not. const cleaned = returnUrlFromLocation({ pathname: "/", - search: "?post=abc&code=xyz&state=s&session_state=t&iss=i", + search: "?post=abc&code=xyz&state=s&session_state=t&iss=i&error=access_denied&error_description=private&error_uri=https://example.test/error", hash: "", }); expect(cleaned).toBe("/?post=abc"); }); + it("sanitizes callback artifacts in returned state and stored fallbacks", () => { + const callbackPath = "/?post=abc&error=access_denied&error_description=private&code=one-time#evidence"; + expect(restoreOidcReturnUrl({ returnUrl: callbackPath })).toBe("/?post=abc#evidence"); + window.localStorage.setItem("lineageweave.oidc.returnUrl", callbackPath); + expect(restoreOidcReturnUrl(undefined)).toBe("/?post=abc#evidence"); + rememberOidcReturnUrl(callbackPath); + expect(window.sessionStorage.getItem("lineageweave.oidc.returnUrl")).toBe("/?post=abc#evidence"); + expect(window.localStorage.getItem("lineageweave.oidc.returnUrl")).toBe("/?post=abc#evidence"); + }); + + it.each(["/\\evil.example", "/\t/evil.example", "/\\["])("rejects paths that URL parsing would turn into another authority (%j)", (unsafePath) => { + rememberOidcReturnUrl(unsafePath); + expect(window.localStorage.getItem("lineageweave.oidc.returnUrl")).toBeNull(); + expect(restoreOidcReturnUrl({ returnUrl: unsafePath })).toBe("/"); + }); + it("restores an object or serialized OIDC state before storage fallback", () => { rememberOidcReturnUrl("/?post=stored-before-direct"); expect(restoreOidcReturnUrl("/?post=from-direct-state")).toBe( @@ -83,7 +99,7 @@ describe("OIDC return URL handling", () => { describe("stripOidcCallbackParams", () => { it("removes the Keycloak auth-exchange params but keeps app deep-link params", () => { const url = new URL( - "http://localhost:15173/?state=abc&session_state=def&iss=http%3A%2F%2Fidp&code=xyz&post=post-1&workspace=board", + "http://localhost:15173/?state=abc&session_state=def&iss=http%3A%2F%2Fidp&code=xyz&error=access_denied&error_description=private&error_uri=https://example.test/error&post=post-1&workspace=board", ); stripOidcCallbackParams(url); @@ -92,6 +108,9 @@ describe("stripOidcCallbackParams", () => { expect(url.searchParams.get("session_state")).toBeNull(); expect(url.searchParams.get("iss")).toBeNull(); expect(url.searchParams.get("code")).toBeNull(); + expect(url.searchParams.get("error")).toBeNull(); + expect(url.searchParams.get("error_description")).toBeNull(); + expect(url.searchParams.get("error_uri")).toBeNull(); expect(url.searchParams.get("post")).toBe("post-1"); expect(url.searchParams.get("workspace")).toBe("board"); }); From 4f17693b992dd59b1b6832469f9eccc174509080 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 7 Sep 2026 10:03:58 +0900 Subject: [PATCH 2/7] fix(auth): recover failed sign-ins with sanitized destinations --- .../adr/0109-oidc-deep-link-state-recovery.md | 15 ++++++++ docs/adr/0220-token-backed-status-notice.md | 23 ++++++++++++ frontend/src/App.tsx | 35 ++++++++++--------- frontend/src/oidcReturnUrl.ts | 31 +++++++++------- 4 files changed, 75 insertions(+), 29 deletions(-) diff --git a/docs/adr/0109-oidc-deep-link-state-recovery.md b/docs/adr/0109-oidc-deep-link-state-recovery.md index 808f12aff..f38b2b6c9 100644 --- a/docs/adr/0109-oidc-deep-link-state-recovery.md +++ b/docs/adr/0109-oidc-deep-link-state-recovery.md @@ -23,6 +23,14 @@ when the member's OIDC session is otherwise valid. recovery fallback, not an authentication or authorization store. - On callback, remove the key from both stores and use session storage before local storage. Reject external and protocol-relative URLs. +- Validate the browser-parsed origin as well as the leading slash: backslashes + and embedded whitespace must not turn a path into another authority. Invalid + URL syntax fails closed. Strip authorization success and error response + parameters before storing, restoring, or sharing a path (RFC 6749 sections + 4.1.2 and 4.1.2.1); retain application parameters and fragments. +- A failed sign-in keeps an explicit retry action (ADR 0220). Before retrying, + consume the existing return-path fallback and remember its sanitized value + again so a failed callback does not replace the intended post destination. - Keep member language preference account-scoped in `user_account.preferred_locale`; this ADR does not move locale state into the post URL, browser storage, or a `user_account + post_id` key. @@ -33,3 +41,10 @@ Opening a shared post link survives a missing OIDC state payload or a changed storage context without losing the post. A stale internal return path is removed at callback, and authorization still comes only from the authenticated OIDC token and backend ABAC checks. + +## References — APA 7th + +Hardt, D. (Ed.). (2012). *The OAuth 2.0 authorization framework* (RFC 6749). +Internet Engineering Task Force. https://www.rfc-editor.org/rfc/rfc6749.html + +WHATWG. (n.d.). *URL standard*. https://url.spec.whatwg.org/ diff --git a/docs/adr/0220-token-backed-status-notice.md b/docs/adr/0220-token-backed-status-notice.md index 1ae329ee4..fabb60764 100644 --- a/docs/adr/0220-token-backed-status-notice.md +++ b/docs/adr/0220-token-backed-status-notice.md @@ -37,6 +37,29 @@ The first migrated product flow is the Calendar Naruon fail-closed path. Do not copy closed-branch exception classes or Storybook inventories from PR #490. Later unavailable flows migrate one at a time. +### Sign-in recovery + +A failed sign-in or an incomplete authenticated session keeps the existing +login card, with product heading, a bounded retry notice, and one **Log in** +action. Never display the authentication library's error message or credential +details. Reuse the existing translated failure message and button label; add +no translation catalog or identity transport. The action retries the existing +OIDC flow with the sanitized destination from ADR 0109. Loading retains the +existing live region and offers no duplicate submission action; successful +authentication continues into the authorized workspace. + +Reuse the login layout and `StatusNotice` tokens, semantic alert, native +button, keyboard behavior, and minimum control size. Storybook records the +sign-in retry variant, and browser checks cover desktop and narrow layouts. +The existing Figma file `1Su3lDRmiZdcUs47t1QwIX`, page `0:1`, was inspected on +2026-09-07: its Event Lineage and Ask Agent frames do not define a sign-in +screen. This repair therefore makes no sign-in Figma parity claim. + +This migration supplies a recovery action for the existing login journey. +Product-owned login, enrollment, and account recovery forms still require the +released Keyverse contracts specified by the product goal; a retry button +does not establish those contracts. + ## Consequences - Calendar names the missing Naruon projection and the next action in one diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e3fb6c796..be2feddae 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -96,6 +96,7 @@ import { CutoffKnownBody } from "./components/CutoffKnownBody"; import { LineageEntityPicker } from "./components/LineageEntityPicker"; import { PopupCloseButton } from "./components/PopupCloseButton"; import { TeppAcceptedReceipt } from "./components/TeppAcceptedReceipt"; +import { StatusNotice } from "./components/StatusNotice"; import { chatEvidenceKindLabel } from "./evidenceKindLabels"; import { WorkspaceNav, type WorkspaceDestination } from "./components/WorkspaceNav"; import { OccupationRatingProfile } from "./components/OccupationRatingProfile"; @@ -107,6 +108,7 @@ import { isFocusableVisible } from "./focusVisibility"; import { subgraphForPost } from "./lineageLayout"; import { rememberOidcReturnUrl, + restoreOidcReturnUrl, returnUrlFromLocation, stripOidcCallbackParams, } from "./oidcReturnUrl"; @@ -5301,11 +5303,13 @@ export default function App({ showLabPanels = false }: { showLabPanels?: boolean return

{t("Loading authentication state...")}

; } - if (auth.error) { - return

{t(auth.error.message)}

; - } - - if (!auth.isAuthenticated) { + if (auth.error || !auth.isAuthenticated || !accessToken) { + const needsRetry = Boolean(auth.error || auth.isAuthenticated); + const signIn = () => { + const returnUrl = needsRetry ? restoreOidcReturnUrl(undefined) : returnUrlFromLocation(); + rememberOidcReturnUrl(returnUrl); + void auth.signinRedirect({ state: { returnUrl } }); + }; return (
@@ -5315,13 +5319,16 @@ export default function App({ showLabPanels = false }: { showLabPanels?: boolean

Marketing & Operational Lineage Intelligence

- + {needsRetry ? ( + + ) : ( + + )}
Enterprise SSO Authentication @@ -5340,10 +5347,6 @@ export default function App({ showLabPanels = false }: { showLabPanels?: boolean ); } - if (!accessToken) { - return

{t("Authenticated, but no access token was returned.")}

; - } - return (
diff --git a/frontend/src/oidcReturnUrl.ts b/frontend/src/oidcReturnUrl.ts index 5d478668c..56622eb0d 100644 --- a/frontend/src/oidcReturnUrl.ts +++ b/frontend/src/oidcReturnUrl.ts @@ -1,11 +1,13 @@ export const OIDC_RETURN_URL_STORAGE_KEY = "lineageweave.oidc.returnUrl"; const MAX_OIDC_RETURN_URL_LENGTH = 4096; -/** Authorization-code response params Keycloak appends to the redirect URI - * (RFC 6749 sec. 4.1.2; `session_state` per OIDC Session Management). Any +/** Authorization response params Keycloak appends to the redirect URI + * (RFC 6749 sec. 4.1.2 and 4.1.2.1; `session_state` per OIDC Session Management). Any * link built from `window.location` must strip these -- they're a one-time * auth exchange, never part of a shareable URL. */ -const OIDC_CALLBACK_PARAMS = ["code", "state", "session_state", "iss"] as const; +const OIDC_CALLBACK_PARAMS = [ + "code", "state", "session_state", "iss", "error", "error_description", "error_uri", +] as const; /** Removes OIDC callback artifacts from `url` in place -- call before turning * `window.location` into a link a user can copy or share. */ @@ -16,11 +18,14 @@ export function stripOidcCallbackParams(url: URL): void { type UrlLike = Pick; function isSafeReturnUrl(value: string): boolean { - return ( - value.length <= MAX_OIDC_RETURN_URL_LENGTH && - value.startsWith("/") && - !value.startsWith("//") - ); + if (value.length > MAX_OIDC_RETURN_URL_LENGTH || !value.startsWith("/") || value.startsWith("//")) { + return false; + } + try { + return new URL(value, window.location.origin).origin === window.location.origin; + } catch { + return false; + } } export function returnUrlFromLocation(location: UrlLike = window.location): string { @@ -37,13 +42,14 @@ export function returnUrlFromLocation(location: UrlLike = window.location): stri export function rememberOidcReturnUrl(value: string): void { if (!isSafeReturnUrl(value)) return; + const returnUrl = returnUrlFromLocation(new URL(value, window.location.origin)); try { - window.sessionStorage.setItem(OIDC_RETURN_URL_STORAGE_KEY, value); + window.sessionStorage.setItem(OIDC_RETURN_URL_STORAGE_KEY, returnUrl); } catch { // OIDC state remains the fallback when session storage is unavailable. } try { - window.localStorage.setItem(OIDC_RETURN_URL_STORAGE_KEY, value); + window.localStorage.setItem(OIDC_RETURN_URL_STORAGE_KEY, returnUrl); } catch { // The OIDC state and session storage remain the fallbacks. } @@ -83,9 +89,8 @@ export function restoreOidcReturnUrl(state: unknown): string { } catch { // Fall through to the current path. } - if (fromState) return fromState; - if (isSafeReturnUrl(sessionStored)) return sessionStored; - if (isSafeReturnUrl(localStored)) return localStored; + const returnUrl = [fromState, sessionStored, localStored].find(isSafeReturnUrl); + if (returnUrl) return returnUrlFromLocation(new URL(returnUrl, window.location.origin)); return new URLSearchParams(window.location.search).has("post") ? returnUrlFromLocation() : window.location.pathname; From 221df228102c96ad0c64fb46868c714de13b1d8c Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 7 Sep 2026 10:06:09 +0900 Subject: [PATCH 3/7] test(auth): cover localized keyboard recovery and retry story --- docs/storybook-inventory.md | 2 +- frontend/src/App.test.tsx | 14 +++++++++----- frontend/src/components/StatusNotice.stories.tsx | 15 +++++++++++++++ 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/docs/storybook-inventory.md b/docs/storybook-inventory.md index f426285a6..d2baf415c 100644 --- a/docs/storybook-inventory.md +++ b/docs/storybook-inventory.md @@ -19,7 +19,7 @@ operator-facing control you can click before changing product CSS. | `Analysis/LineageEntityPicker` | Choose which corp to reconstruct, then click Request a lineage reconstruction. | `--space-control-gap`, `--size-control-min`, `--radius-control`, `LineageEntityPicker` | | `Admin/AdminPanel` | Change the tenant brand name, then verify the saved or failed state before leaving settings. | `--surface`, `--border`, `--space-panel-block`, `AdminPanel` | | `Lineage/LineageDag` | Open a reconstructed connection to read its inferred channel scores and Allen interval relation, or open the current branch node; compare empty, single-branch, grouped/forked, mobile-scroll, ungrouped, and long-title states before changing graph CSS. On narrow viewports, swipe the named viewport or focus it and use arrow keys to inspect the full lineage. | `--color-accent-background`, `--radius-control`, `--surface`, `--border`, `--color-focus-border`, `--size-control-min`, `LineageDag` | -| `Chrome/StatusNotice` | Read success, unavailable, or retry copy, then take the named next action. Success and unavailable are a named region (not live `role=status`); Retry is `role=alert` and only on the retry kind. Calendar's missing Naruon projection uses unavailable. | `--badge-status-success-*`, `--badge-status-pending-*`, `--badge-status-danger-*`, `StatusNotice` | +| `Chrome/StatusNotice` | Read success, unavailable, or retry copy, then take the named next action. Success and unavailable are a named region (not live `role=status`); Retry is `role=alert` and only on the retry kind. Calendar's missing Naruon projection uses unavailable. SignInRetry provides the Log in action after a failed sign-in. | `--badge-status-success-*`, `--badge-status-pending-*`, `--badge-status-danger-*`, `StatusNotice` | | `Chrome/PopupCloseButton` | Close the evidence panel or post popup. | `--space-close-inset`, `--font-size-close`, `PopupCloseButton` | | `Workspace/WorkspaceCalendar` | Read observed Naruon events, or open a commitment to land on that post. Fail-closed copy stays `이 범위의 일정을 아직 받을 수 없습니다`. | `--color-chip-border`, `WorkspaceCalendar`, `EvidenceStatusMark` | | `Ask Agent/Public claim verification` | Compare supported, refuted, and not-enough-information states; open only the external evidence link, then review the separate internal citation before changing governed graph state. | `--space-panel-block`, `--space-control-gap`, `--color-border`, `--size-control-min`, `PublicClaimVerification` | diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index 8dbea2c20..5414f3e07 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -3,7 +3,7 @@ import userEvent from "@testing-library/user-event"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import App, { SurfaceBoundary } from "./App"; import { optionalKnowledgeCutoffIso } from "./api"; -import { setLocale } from "./i18n"; +import { setLocale, SUPPORTED_LOCALES, t } from "./i18n"; import { OIDC_RETURN_URL_STORAGE_KEY } from "./oidcReturnUrl"; const signinRedirect = vi.fn(); @@ -74,7 +74,8 @@ it("announces a lazy surface load failure with a recovery action", () => { describe("App, unauthenticated", () => { - it("offers a safe sign-in retry and retains the pre-callback destination", async () => { + it.each(SUPPORTED_LOCALES)("offers a safe sign-in retry and retains the pre-callback destination (%s)", async (locale) => { + setLocale(locale); const privateError = "synthetic-private-auth-response"; window.history.replaceState({}, "", `/?error=access_denied&error_description=${privateError}&state=stale`); window.sessionStorage.setItem(OIDC_RETURN_URL_STORAGE_KEY, "/?post=remembered#evidence"); @@ -82,9 +83,11 @@ describe("App, unauthenticated", () => { render(); - expect(screen.getByRole("alert")).toHaveTextContent("This request failed. Retry the same action."); + expect(screen.getByRole("alert")).toHaveTextContent(t("This request failed. Retry the same action.")); expect(screen.queryByText(privateError)).not.toBeInTheDocument(); - await userEvent.click(screen.getByRole("button", { name: "Log in" })); + await userEvent.tab(); + expect(screen.getByRole("button", { name: t("Log in") })).toHaveFocus(); + await userEvent.keyboard("{Enter}"); expect(signinRedirect).toHaveBeenCalledWith({ state: { returnUrl: "/?post=remembered#evidence" } }); expect(window.localStorage.getItem(OIDC_RETURN_URL_STORAGE_KEY)).toBe("/?post=remembered#evidence"); }); @@ -120,9 +123,10 @@ describe("App, unauthenticated", () => { }); it("announces the app-root auth loading gate as a live region", () => { - mockAuth = { ...mockAuth, isLoading: true }; + mockAuth = { ...mockAuth, isLoading: true, error: new Error("previous failure") }; render(); expect(screen.getByRole("status")).toHaveTextContent("Loading authentication state..."); + expect(screen.queryByRole("button")).not.toBeInTheDocument(); }); }); diff --git a/frontend/src/components/StatusNotice.stories.tsx b/frontend/src/components/StatusNotice.stories.tsx index 89ad543b4..55827d78b 100644 --- a/frontend/src/components/StatusNotice.stories.tsx +++ b/frontend/src/components/StatusNotice.stories.tsx @@ -63,3 +63,18 @@ export const Retry: Story = { await expect(args.onRetry).toHaveBeenCalledTimes(1); }, }; + +export const SignInRetry: Story = { + args: { + kind: "retry", + message: "This request failed. Retry the same action.", + retryLabel: "Log in", + onRetry: fn(), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + await expect(canvas.getByRole("alert")).toHaveTextContent("Retry needed"); + await userEvent.click(canvas.getByRole("button", { name: "Log in" })); + await expect(args.onRetry).toHaveBeenCalledTimes(1); + }, +}; From 5970c4270e57f3b637f615dabe1d2547eda9e5e5 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 7 Sep 2026 10:12:22 +0900 Subject: [PATCH 4/7] fix(auth): include login card padding in its responsive width --- docs/adr/0220-token-backed-status-notice.md | 2 ++ frontend/src/App.css | 1 + 2 files changed, 3 insertions(+) diff --git a/docs/adr/0220-token-backed-status-notice.md b/docs/adr/0220-token-backed-status-notice.md index fabb60764..312086203 100644 --- a/docs/adr/0220-token-backed-status-notice.md +++ b/docs/adr/0220-token-backed-status-notice.md @@ -51,6 +51,8 @@ authentication continues into the authorized workspace. Reuse the login layout and `StatusNotice` tokens, semantic alert, native button, keyboard behavior, and minimum control size. Storybook records the sign-in retry variant, and browser checks cover desktop and narrow layouts. +The login card includes padding and borders in its declared width, so the +page's overflow rule cannot hide clipped card edges on narrow screens. The existing Figma file `1Su3lDRmiZdcUs47t1QwIX`, page `0:1`, was inspected on 2026-09-07: its Event Lineage and Ask Agent frames do not define a sign-in screen. This repair therefore makes no sign-in Figma parity claim. diff --git a/frontend/src/App.css b/frontend/src/App.css index 3e4c13599..cb15f6a6b 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -22,6 +22,7 @@ } .login-card { + box-sizing: border-box; width: 100%; max-width: 420px; padding: 2.5rem 2rem; From b0942b516fa9f03e2322da499959894ad2f5f9bb Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 7 Sep 2026 10:17:27 +0900 Subject: [PATCH 5/7] docs(auth): record recovery evidence and remaining owner gaps --- docs/product-technical-gap-baseline.md | 58 ++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index b5d31877b..d2820052a 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1,5 +1,63 @@ # Product & Technical Gap Baseline +## 2026-09-07: Recover interrupted sign-in without losing the destination + +This dated observation supersedes earlier loop summaries for this scope only. +Protected `main` was `83eba56149eb802cd63642c507c324c9976ec78e` at inspection. +The repair is proposed on `codex/signin-recovery-20260907`; the original +translation-ledger checkout and its uncommitted work were left intact. + +**Observed failure.** In an isolated browser session, an unavailable loopback +issuer reduced the entire login screen to `Failed to fetch`, with no action +to recover. The other incomplete-session branch exposed credential plumbing. +The shared return-path helper also retained authorization error parameters +and accepted malformed paths that browser URL parsing interpreted as another +authority. This helper serves callback restoration, login, and shared post +links, so the correction belongs at that common boundary (ADR 0109). + +**Repair and KPI.** Test revision `fb4d11435` reproduced six failing URL cases +out of twelve and two failing App recovery cases out of four. The first +combined baseline encountered a worker-start timeout; a separate App run with +the installed thread pool reproduced both failures. Implementation +`4f17693b9` passes all sixteen focused cases: failure count **8 → 0**. +It reuses ADR 0220's retry notice, existing translated copy, and the real OIDC +redirect. A retry consumes and re-saves the sanitized destination; it does +not replace a remembered post with a failed callback. Loading retains its +single live region. No new dependency, issuer, or translation catalog is added. + +```sh +corepack pnpm --dir frontend exec vitest run src/App.test.tsx src/oidcReturnUrl.test.ts --pool=threads --maxWorkers=1 -t 'App, unauthenticated|OIDC return URL|stripOidcCallbackParams' +``` + +**Browser and design evidence.** The local frontend ran on port 15274 with +`VITE_KEYVERSE_ISSUER=http://127.0.0.1:15999` intentionally unavailable. No +credentials or source records were supplied. After the correction, the Korean +retry notice remained available after repeated failures; Tab reached Log in +and Enter retried. At 390px, geometry inspection found the existing login card +clipped eight pixels on each side despite no document-level horizontal +overflow. Revision `5970c4270` includes padding and borders in the card width. +At 320px, its bounds are 21–299px, with readable wrapping and no horizontal +overflow. Desktop and narrow screenshots were inspected in the actual browser. +The existing Figma page has no sign-in frame (ADR 0220 records the file/page +IDs); no sign-in design-parity claim is made. `Chrome/StatusNotice/SignInRetry` +records the recovery action. Full App regression, five-current-locale keyboard +checks, lint, production build, and Storybook build remain pending at this +documentation checkpoint; final results belong on the exact PR head. + +**Remaining acceptance gaps.** This failure-path browser check does not prove +successful authentication, deployed behavior, eight-locale database delivery, +or the all-page p95 ≤20ms goal. None is claimed. Keyverse protected `main` +`7d9151cd2da260e118020c938c7358e2ee75d541` had no published release or tag at +inspection. It implements an authorization-code/PKCE flow and server-mediated +signup enrollment, but no verified product-facing recovery contract. The +adjacent [Keyverse #128](https://github.com/ContextualWisdomLab/keyverse/pull/128) +remains a draft authentication migration; its fail-closed password endpoint +does not fulfill product-owned forms. +[Keyverse #100](https://github.com/ContextualWisdomLab/keyverse/pull/100) is the +separate LineageWeave claims prerequisite. Complete and release those owner +contracts before consumer adoption. The existing eight-locale translation +ledger work remains separate from this recovery repair. + > Exact-head loop overlay: 2026-08-29 13:20 KST. Protected `main` is > `fc13acaa20adca11968238e398d4aafcf62b6cee` (v2.23.0 leftover-map > explained leftover share, #775). Open ready PRs still lack independent From b6059454994815b579ff3c93a8c160af803e8679 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 7 Sep 2026 10:24:04 +0900 Subject: [PATCH 6/7] docs(auth): retain full regression failures alongside passing recovery evidence --- docs/product-technical-gap-baseline.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index d2820052a..5fede3133 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -40,9 +40,17 @@ At 320px, its bounds are 21–299px, with readable wrapping and no horizontal overflow. Desktop and narrow screenshots were inspected in the actual browser. The existing Figma page has no sign-in frame (ADR 0220 records the file/page IDs); no sign-in design-parity claim is made. `Chrome/StatusNotice/SignInRetry` -records the recovery action. Full App regression, five-current-locale keyboard -checks, lint, production build, and Storybook build remain pending at this -documentation checkpoint; final results belong on the exact PR head. +records the recovery action; its three interaction steps passed in the built +Storybook browser. Lint, production build, Storybook build, and five +documentation checks passed. The expanded recovery checks pass in all five +current locales, including keyboard activation. The three-file regression run +at `221df2281` completed with **53 passed, 73 failed (126 total)**: the failures +are in existing authenticated journeys, predominantly test deadlines, with +additional element-lookup failures. The run took 938 seconds; concurrent host +load exceeded 60 and swap use exceeded 44 GB. Contention is a hypothesis, not +proof that the failures are harmless. Keep the PR draft until unchanged +required checks verify the authenticated journeys; do not raise their limits, +skip them, or claim a full regression pass. **Remaining acceptance gaps.** This failure-path browser check does not prove successful authentication, deployed behavior, eight-locale database delivery, From 583157c5d007d4903a78b2e30c0caac224f6460e Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 7 Sep 2026 10:28:34 +0900 Subject: [PATCH 7/7] docs(auth): distinguish baseline failures from review admission --- docs/product-technical-gap-baseline.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 5fede3133..ed0338a4e 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -48,9 +48,13 @@ at `221df2281` completed with **53 passed, 73 failed (126 total)**: the failures are in existing authenticated journeys, predominantly test deadlines, with additional element-lookup failures. The run took 938 seconds; concurrent host load exceeded 60 and swap use exceeded 44 GB. Contention is a hypothesis, not -proof that the failures are harmless. Keep the PR draft until unchanged -required checks verify the authenticated journeys; do not raise their limits, -skip them, or claim a full regression pass. +proof that the failures are harmless. A four-case recheck passed three and +retained one timeout. That remaining case also failed in a paired experiment +using protected-main App and return-path source; temporary experiment files +were removed. The repository skips Tests jobs for drafts and has no manual +dispatch, so the ready-for-review event admits the normal checks. It does not +establish merge readiness. Required checks must verify the authenticated +journeys; do not raise their limits, skip them, or claim a full regression pass. **Remaining acceptance gaps.** This failure-path browser check does not prove successful authentication, deployed behavior, eight-locale database delivery,