Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,11 @@ A run-bearing analysis-run registry empties only after an unrevoked
(ADR 0020 / v0.87.0). The documented phrase is not a secret. Do not
expose purge on a public HTTP route.

Unauthenticated **Log in** must call `returnUrlFromLocation()` then
`rememberOidcReturnUrl` before `signinRedirect` (ADR 0109) so a
shared `/?post=` link still opens that post. Do not mount tenant admin
settings on the signed-out login shell.

`POST /api/analysis-runs` records Pending lineage only (ADR 0017 /
v2.7.1). TEPP and period-report kinds 422 before any snapshot write.
`POST /api/analysis-runs/{id}/start` reconstructs a Pending lineage
Expand Down
4 changes: 3 additions & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,9 @@ config baked in at build time from the same `.env` ports every other
service uses (Vite embeds `import.meta.env.VITE_*` at build time, not
runtime, so these are Docker build args, not container env vars).
`src/App.test.tsx` mocks `react-oidc-context`'s `useAuth` to test the
component's own render logic (login button -> `signinRedirect()`; the
component's own render logic (login button stores a safe return path
then `signinRedirect()`; the signed-out shell never mounts tenant
admin settings; the
A-100 fork DAG shows a branch point and rec-006 as its own root;
`post_admin` can rebuild; fetch posts with the token -> render list ->
click -> popup shows the fetched body and every panel; ask a chat
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.d/2.12.19-oidc-login-return-remember.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
## 2.12.19 — Remember the login return path

- Log in now stores a validated same-origin return path (ADR 0109)
before the OIDC redirect, so a shared `/?post=` link still opens that
post after callback. Tenant admin settings stay off the signed-out
login shell so the production frontend build type-checks.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,16 @@ All notable changes to this project are documented here. Format follows
- The static SQL review contract now counts the Customer Master evidence query
that uses closed schema fragments and bound entity ids.

## [2.12.19] - 2026-08-24

### Fixed

- Log in remembers a validated same-origin return path (ADR 0109) before
the OIDC redirect, so a shared `/?post=` link still opens that post
after callback. Tenant admin settings stay off the signed-out login
shell. The production frontend build type-checks again.


## [2.12.6] - 2026-08-20

### Added
Expand Down
5 changes: 5 additions & 0 deletions docs/adr/0109-oidc-deep-link-state-recovery.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ when the member's OIDC session is otherwise valid.
- Persist the same validated same-origin path in both `sessionStorage` and
`localStorage` before redirecting to OIDC. `localStorage` is only a bounded
recovery fallback, not an authentication or authorization store.
- The signed-out **Log in** control computes that path with
`returnUrlFromLocation()`, persists it with `rememberOidcReturnUrl`, and
passes the same value in `signinRedirect` state.
- Tenant admin settings mount only after authentication provides an access
token; the signed-out login shell never renders `AdminPanel`.
- On callback, remove the key from both stores and use session storage before
local storage. Reject external and protocol-relative URLs.
- Keep member language preference account-scoped in
Expand Down
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "frontend",
"private": true,
"version": "2.12.6",
"version": "2.12.19",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
23 changes: 23 additions & 0 deletions frontend/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ beforeEach(() => {

afterEach(() => {
vi.unstubAllGlobals();
window.sessionStorage.clear();
window.localStorage.clear();
});

describe("App, unauthenticated", () => {
Expand All @@ -55,6 +57,27 @@ describe("App, unauthenticated", () => {
);
});

it("remembers a same-origin post deep link before the OIDC redirect", async () => {
window.history.replaceState({}, "", "/?post=synthetic-post-ada");
render(<App />);
await userEvent.click(screen.getByRole("button", { name: /log in/i }));
expect(window.sessionStorage.getItem("lineageweave.oidc.returnUrl")).toBe(
"/?post=synthetic-post-ada",
);
expect(window.localStorage.getItem("lineageweave.oidc.returnUrl")).toBe(
"/?post=synthetic-post-ada",
);
expect(signinRedirect).toHaveBeenCalledWith({
state: { returnUrl: "/?post=synthetic-post-ada" },
});
});

it("does not mount tenant admin settings before authentication", () => {
render(<App />);
expect(screen.queryByRole("heading", { name: /admin settings/i })).not.toBeInTheDocument();
expect(screen.queryByLabelText(/tenant brand name/i)).not.toBeInTheDocument();
});

it("does not render raw OIDC error text and names a log-in next action", async () => {
mockAuth = {
...mockAuth,
Expand Down
10 changes: 7 additions & 3 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ import {
tf,
useLocale,
} from "./i18n";
import { rememberOidcReturnUrl, returnUrlFromLocation } from "./oidcReturnUrl";
import "./App.css";

function orchestratorUnavailableMessage(err: unknown, action: string): string {
Expand Down Expand Up @@ -6292,7 +6293,8 @@ export default function App({ showLabPanels = false }: { showLabPanels?: boolean
description={t("Log in again to open the workspace.")}
retryLabel={t("Log in")}
onRetry={() => {
const returnUrl = window.location.pathname + window.location.search;
const returnUrl = returnUrlFromLocation();
rememberOidcReturnUrl(returnUrl);
void auth.signinRedirect({ state: { returnUrl } });
}}
/>
Expand All @@ -6314,7 +6316,8 @@ export default function App({ showLabPanels = false }: { showLabPanels?: boolean
</div>
<div className="login-controls">
<button className="btn-primary" onClick={() => {
const returnUrl = window.location.pathname + window.location.search;
const returnUrl = returnUrlFromLocation();
rememberOidcReturnUrl(returnUrl);
Comment thread
seonghobae marked this conversation as resolved.
void auth.signinRedirect({ state: { returnUrl } });
}}>
{t("Log in")}
Expand Down Expand Up @@ -6347,7 +6350,8 @@ export default function App({ showLabPanels = false }: { showLabPanels?: boolean
description={t("Log in again to open the workspace.")}
retryLabel={t("Log in")}
onRetry={() => {
const returnUrl = window.location.pathname + window.location.search;
const returnUrl = returnUrlFromLocation();
rememberOidcReturnUrl(returnUrl);
void auth.signinRedirect({ state: { returnUrl } });
}}
/>
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "lineageweave"
version = "2.12.6"
version = "2.12.19"
description = "Reconstructs git-branch-style lineage DAGs from scattered short records using multi-channel score fusion and LLM adjudication."
readme = "README.md"
license = { text = "MIT" }
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading