Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 / 0119) 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 0119 / 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 @@ -12,6 +12,16 @@ All notable changes to this project are documented here. Format follows
environment, so local OIDC and synthetic-data workflows resolve the same
pinned dependencies as CI.

## [2.12.19] - 2026-08-24

### Fixed

- Log in remembers a validated same-origin return path (ADR 0119) 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
1 change: 1 addition & 0 deletions docs/adr/0109-oidc-deep-link-state-recovery.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
- Status: Accepted
- Date: 2026-08-20
- Depends on: [0069](0069-member-locale-preference.md), [0028](0028-keyverse-oidc-provider.md)
- Amended by: [0119](0119-oidc-login-remembers-return-path.md)

## Context

Expand Down
39 changes: 39 additions & 0 deletions docs/adr/0119-oidc-login-remembers-return-path.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# ADR 0119 — Login remembers a safe return path and does not mount admin settings

**Decision status:** Accepted
**Date:** 2026-08-24
**Amends:** [ADR 0109](0109-oidc-deep-link-state-recovery.md)

## Context

ADR 0109 recovers `/?post=<id>` across OIDC callback contexts by persisting a
validated same-origin path before redirect and restoring it on callback.
The unauthenticated login control later stopped calling
`rememberOidcReturnUrl` / `returnUrlFromLocation`, so the callback helpers
had nothing to restore when provider state was omitted. The same login shell
also type-checked an `AdminPanel` with an undefined access token, which
broke the frontend production build (`tsc -b`) and would have asked a
signed-out buyer to save tenant settings.

## Decision

- The unauthenticated **Log in** control takes `returnUrl` from
`returnUrlFromLocation()`, calls `rememberOidcReturnUrl(returnUrl)`, then
starts `signinRedirect({ state: { returnUrl } })`. Raw
`pathname + search` concatenation is not a return URL.
- Tenant admin settings mount only after authentication has produced an
access token. The signed-out login shell does not render `AdminPanel`.
- Do not invent a leftover score, a theta, or a tenant name. Synthetic
fixtures only.

## Consequences

Opening a shared post link, logging in, and landing on that post works when
the provider omits application state. `pnpm run build` type-checks the login
shell. Independent of leftover-map PRs (#481, #485, #518, #519, #521, #522).

## References

Miles, A., & Bechhofer, S. (Eds.). (2009). *SKOS simple knowledge
organization system reference*. World Wide Web Consortium.
https://www.w3.org/TR/skos-reference/
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
26 changes: 26 additions & 0 deletions frontend/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ afterEach(() => {
});

describe("App, unauthenticated", () => {
beforeEach(() => {
window.sessionStorage.clear();
window.localStorage.clear();
});

it("shows a login button that starts the real OIDC redirect", async () => {
render(<App showLabPanels />);
const button = screen.getByRole("button", { name: /log in/i });
Expand All @@ -42,6 +47,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();
});
});

function jsonResponse(body: unknown): Response {
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4610,7 +4610,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.
Resolved
void auth.signinRedirect({ state: { returnUrl } });
}}>
{t("Log in")}
Expand All @@ -4620,7 +4621,6 @@ export default function App({ showLabPanels = false }: { showLabPanels?: boolean
<small>Enterprise SSO Authentication</small>
</div>
</div>
{destination === "admin" ? <AdminPanel currentBrandName={brandName} onBrandNameChange={setBrandName} accessToken={accessToken} /> : null}
</main>
<footer className="app-footer" role="contentinfo">
<div className="app-footer-title">
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