From 14f0b52373beef2f7700bab93ff50c3cb3fc81f2 Mon Sep 17 00:00:00 2001 From: Muawiya Amir Date: Sun, 25 Oct 2026 13:06:29 +0500 Subject: [PATCH 01/15] fix(edge): restore public auth and privacy landing pages --- supabase/functions/api/handler.test.mjs | 26 ++++ supabase/functions/api/handler.ts | 3 + supabase/functions/api/public_pages.ts | 156 ++++++++++++++++++++++++ 3 files changed, 185 insertions(+) create mode 100644 supabase/functions/api/public_pages.ts diff --git a/supabase/functions/api/handler.test.mjs b/supabase/functions/api/handler.test.mjs index 6b08476c..394b2e5f 100644 --- a/supabase/functions/api/handler.test.mjs +++ b/supabase/functions/api/handler.test.mjs @@ -14,6 +14,32 @@ const config = { const request = (path, init = {}) => new Request(`https://one-concept.supabase.co/functions/v1/api${path}`, init); const editorialToken = (claims) => `header.${Buffer.from(JSON.stringify(claims)).toString('base64url')}.signature`; +test('public cutover pages serve browser HTML without leaking service credentials', async () => { + const calls = []; + const handler = createHandler(config, async (...args) => { + calls.push(args); + throw new Error('Public pages must not use a service request'); + }); + for (const path of ['/confirmed', '/reset-password', '/privacy']) { + const result = await handler(request(path)); + const body = await result.text(); + assert.equal(result.status, 200); + assert.match(result.headers.get('content-type'), /^text\/html/); + assert.equal(result.headers.get('cache-control'), 'no-store'); + assert.equal(result.headers.get('referrer-policy'), 'no-referrer'); + assert.match(result.headers.get('content-security-policy'), /frame-ancestors 'none'/); + assert.doesNotMatch(body, /private-service-key|__NONCE__|__CONFIG__/); + } + assert.equal(calls.length, 0); + const reset = await (await handler(request('/reset-password'))).text(); + assert.match(reset, /\/auth\/v1\/user/); + assert.match(reset, /public-anon-key/); + assert.match(reset, /history\.replaceState\(null, '', location\.pathname\)/); + const rejected = await handler(request('/reset-password', { method: 'POST' })); + assert.equal(rejected.status, 405); + assert.equal(rejected.headers.get('allow'), 'GET'); +}); + test('editorial correction request reaches the atomic job RPC with the receipt fingerprint', async () => { const sessionId = 'a5a94932-62e0-4780-91c7-af48b93e27ab'; const revisionId = 'f285ee8a-0bdd-4a94-ac16-ce96c9c3a272'; diff --git a/supabase/functions/api/handler.ts b/supabase/functions/api/handler.ts index f4d480c1..ecd32373 100644 --- a/supabase/functions/api/handler.ts +++ b/supabase/functions/api/handler.ts @@ -2,6 +2,7 @@ import { conceptPreview, editorialDigest, revisionPreview, stageLessonBody } from './editorial_preview.ts'; import { AvatarInputError, normalizeAvatar } from './avatar_image.ts'; +import { publicPage } from './public_pages.ts'; export type ApiConfig = { supabaseUrl: string; @@ -1058,6 +1059,8 @@ export function createHandler(config: ApiConfig, fetcher: Fetcher = fetch, const requestUrl = new URL(request.url); const path = routePath(requestUrl.pathname); if (path === null) return httpError(404, 'Not Found', cors); + const page = publicPage(path, request.method, config, cors); + if (page) return page; const concept = /^\/v1\/concepts\/([^/]+?)(?:\/(like|save))?$/.exec(path); const publicProfile = /^\/v1\/public-profiles\/([^/]+)$/.exec(path); const connectionStatus = /^\/v1\/me\/connections\/with\/([^/]+)$/.exec(path); diff --git a/supabase/functions/api/public_pages.ts b/supabase/functions/api/public_pages.ts new file mode 100644 index 00000000..0edaf90c --- /dev/null +++ b/supabase/functions/api/public_pages.ts @@ -0,0 +1,156 @@ +/** Browser landing pages retained when the learner API moved off Railway. */ + +type PageConfig = { supabaseUrl: string; anonKey: string }; + +const STYLE = ` + body { margin: 0; min-height: 100vh; display: grid; place-items: center; + background: #0f1115; color: #e8eaf0; font: 16px/1.6 system-ui, sans-serif; } + main { width: 100%; max-width: 420px; padding: 2rem; box-sizing: border-box; } + h1 { font-size: 1.6rem; margin: 0 0 .5rem; } + p, label { color: #aeb5c3; } + form { display: grid; gap: .75rem; } + label { display: block; font-size: .9rem; } + input { width: 100%; box-sizing: border-box; padding: .75rem; border-radius: 10px; + border: 1px solid #3d4658; background: #171a21; color: #e8eaf0; font: inherit; } + input:focus { outline: 2px solid #8da5ff; } + button { padding: .8rem; border: 0; border-radius: 10px; background: #8da5ff; + color: #0f1115; font: inherit; font-weight: 700; cursor: pointer; } + button:disabled { opacity: .6; cursor: default; } + .error { color: #ffaaaa; } .success { color: #9cebb5; } + .hidden { display: none; } +`; + +const CONFIRMED = ` + +One Concept — email confirmed +

Email confirmed ✓

+

You're all set. Open the One Concept app and sign in to get today's concept.

+
`; + +const RESET = ` + +One Concept — reset password +

Reset your password

+

Choose a new password for your One Concept account.

+ +
`; + +// Keep this learner-facing disclosure aligned with backend/app/privacy.py. +const PRIVACY = ` + +One Concept — Privacy Policy

Privacy Policy

Effective: October 1, 2026

+

One Concept is operated by Coding Moves. This policy explains the data used to provide the app and how to contact us about it.

+

Information used to provide the app

+

How we use information

We use this information to sign you in, select and remember learning activities, calculate streaks, keep saved and liked content, and send enabled reminders. We do not use the app for advertising or include behavioural analytics or crash-reporting SDKs.

+

Optional public profile

Profile sharing is off by default. If enabled, anyone with your link or QR code sees only the display name, streaks, learned-concept count and achievements you select. Your email, saved concepts, account identifiers and private activity are excluded. Turning sharing off revokes the link, but cannot erase screenshots already made.

+

Optional Connections

Connections stores requests, accepted relationships and blocks for your private lists. Receiving requests is off by default. Other learners see only the profile information you currently share. You can decline, cancel, remove or block a connection. Turn off profile sharing to revoke a public link. Limited request-event timestamps enforce invitation limits.

+

Service providers

Supabase provides authentication, database and API infrastructure. Expo delivers enabled push notifications. These providers process the information needed to run the app.

+

Choices and retention

You can turn reminders off in the app. Signing out removes account-scoped app caches and attempts to deregister the current device while online. Account and learning data remain associated with your account until you request deletion or we no longer need them to run the service.

+

Security

Private requests use a verified account token, and access controls are designed to limit data to the signed-in account. No internet transmission or storage method is completely secure.

+

Your requests and contact

Request access to, correction of, or deletion of your account data by emailing contactmuawia@gmail.com from your account address. We may need to verify the request.

+

Changes

We may update this policy as data practices change. The current version is published at this address with its effective date.

+
`; + +function configLiteral(config: PageConfig): string { + return JSON.stringify({ url: config.supabaseUrl.replace(/\/+$/, ''), anonKey: config.anonKey }) + .replace(/[<>&\u2028\u2029]/g, character => + `\\u${character.charCodeAt(0).toString(16).padStart(4, '0')}`); +} + +export function publicPage(path: string, method: string, config: PageConfig, cors: Headers): Response | null { + if (!['/confirmed', '/reset-password', '/privacy'].includes(path)) return null; + if (method !== 'GET') { + const headers = new Headers(cors); + headers.set('content-type', 'application/json; charset=utf-8'); + headers.set('allow', 'GET'); + return new Response(JSON.stringify({ detail: 'Method Not Allowed' }), { status: 405, headers }); + } + const nonce = crypto.randomUUID().replaceAll('-', ''); + const template = path === '/confirmed' ? CONFIRMED : path === '/privacy' ? PRIVACY : RESET; + const body = template.replaceAll('__NONCE__', nonce) + .replace('__CONFIG__', () => configLiteral(config)); + const headers = new Headers(cors); + headers.set('content-type', 'text/html; charset=utf-8'); + headers.set('cache-control', 'no-store'); + headers.set('x-content-type-options', 'nosniff'); + headers.set('referrer-policy', 'no-referrer'); + headers.set('x-frame-options', 'DENY'); + headers.set('content-security-policy', `default-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'; style-src 'nonce-${nonce}'; script-src 'nonce-${nonce}'; connect-src ${config.supabaseUrl}`); + return new Response(body, { status: 200, headers }); +} From 2ab8a3a71af3c13463bcb8732d116a1112248a8d Mon Sep 17 00:00:00 2001 From: Muawiya Amir Date: Sun, 25 Oct 2026 13:14:36 +0500 Subject: [PATCH 02/15] fix(admin): point learner privacy link at Supabase --- admin/public/privacy.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/admin/public/privacy.html b/admin/public/privacy.html index 65456bab..dd555088 100644 --- a/admin/public/privacy.html +++ b/admin/public/privacy.html @@ -13,7 +13,7 @@

Privacy policy

How we handle review workspace data

Effective October 4, 2026

-

One Concept Review is a private editorial workspace operated by Coding Moves. This policy describes its accounts, review records, and optional email notifications. The One Concept learning app privacy policy covers learner accounts and app data.

+

One Concept Review is a private editorial workspace operated by Coding Moves. This policy describes its accounts, review records, and optional email notifications. The One Concept learning app privacy policy covers learner accounts and app data.

Editorial accounts and work

Supabase Auth handles sign-in, password recovery, and authenticator enrollment. We use an invited reviewer's email address, approved name, permissions, and timezone to manage access and assignments. Our backend records assigned lessons, review comments, requested corrections, decisions, publication history, and related timestamps. Approved reviewer names may appear in the details of lessons they verify.

From 9dfd2eb14d8c2451aa3a281b6e637f99dc3b537b Mon Sep 17 00:00:00 2001 From: Muawiya Amir Date: Wed, 28 Oct 2026 17:35:23 +0500 Subject: [PATCH 03/15] fix(auth): avoid false confirmation on direct page visits (cherry picked from commit 0b1d3af7b9efffd55d0f9b6a1afe822cb3081d3c) --- supabase/functions/api/handler.test.mjs | 36 +++++++++++++++++++++++++ supabase/functions/api/public_pages.ts | 8 ++++-- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/supabase/functions/api/handler.test.mjs b/supabase/functions/api/handler.test.mjs index 394b2e5f..27b2a295 100644 --- a/supabase/functions/api/handler.test.mjs +++ b/supabase/functions/api/handler.test.mjs @@ -1,6 +1,7 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import { createHash } from 'node:crypto'; +import { runInNewContext } from 'node:vm'; import { createHandler } from './handler.ts'; import './avatar.test.mjs'; @@ -40,6 +41,41 @@ test('public cutover pages serve browser HTML without leaking service credential assert.equal(rejected.headers.get('allow'), 'GET'); }); +test('confirmation page claims success only after an Auth redirect', async () => { + const handler = createHandler(config); + const body = await (await handler(request('/confirmed'))).text(); + const script = body.match(/`; From 32fd36fd1990a1d6da0a140bec2a0f40d16a7cb1 Mon Sep 17 00:00:00 2001 From: Muawiya Amir Date: Wed, 28 Oct 2026 18:39:43 +0500 Subject: [PATCH 04/15] test(auth): cover Edge confirmation and recovery in Chromium --- admin/tests/auth-landing.spec.ts | 67 ++++++++++++++++ supabase/functions/api/handler.test.mjs | 101 ++++++++++++++++++++++++ 2 files changed, 168 insertions(+) create mode 100644 admin/tests/auth-landing.spec.ts diff --git a/admin/tests/auth-landing.spec.ts b/admin/tests/auth-landing.spec.ts new file mode 100644 index 00000000..5a556498 --- /dev/null +++ b/admin/tests/auth-landing.spec.ts @@ -0,0 +1,67 @@ +import { expect, test, type Page } from "@playwright/test"; +import { publicPage } from "../../supabase/functions/api/public_pages.ts"; + +const origin = "https://one-concept.supabase.co"; +const base = `${origin}/functions/v1/api`; +const config = { supabaseUrl: origin, anonKey: "public-anon-key" }; + +async function serveAuthPages(page: Page) { + await page.route(`${base}/**`, async route => { + const url = new URL(route.request().url()); + const response = publicPage(url.pathname.slice("/functions/v1/api".length), "GET", config, new Headers()); + if (!response) return route.fulfill({ status: 404, body: "Not found" }); + return route.fulfill({ + status: response.status, + headers: Object.fromEntries(response.headers), + body: await response.text(), + }); + }); +} + +test("direct confirmation visit does not claim that an email was verified", async ({ page }) => { + await serveAuthPages(page); + await page.goto(`${base}/confirmed`); + await expect(page.getByRole("heading", { name: "Check your email link" })).toBeVisible(); + await expect(page.getByText("Email confirmed ✓")).toHaveCount(0); +}); + +test("recovery link reaches the reset form and updates the password with its token", async ({ page }) => { + await serveAuthPages(page); + let authRequest: { method: string; authorization: string | undefined; apikey: string | undefined; password: string } | undefined; + await page.route(`${origin}/auth/v1/user`, async route => { + authRequest = { + method: route.request().method(), + authorization: route.request().headers().authorization, + apikey: route.request().headers().apikey, + password: route.request().postDataJSON().password, + }; + await route.fulfill({ status: 200, contentType: "application/json", body: "{}" }); + }); + + await page.goto(`${base}/confirmed#access_token=recovery-token&type=recovery`); + await expect(page).toHaveURL(`${base}/reset-password`); + await page.getByLabel("New password", { exact: true }).fill("safe-new-password"); + await page.getByLabel("Confirm new password").fill("safe-new-password"); + await page.getByRole("button", { name: "Update password" }).click(); + await expect(page.getByRole("alert")).toContainText("Password updated"); + expect(authRequest).toEqual({ + method: "PUT", + authorization: "Bearer recovery-token", + apikey: "public-anon-key", + password: "safe-new-password", + }); +}); + +test("expired recovery link shows an error and never calls Auth", async ({ page }) => { + await serveAuthPages(page); + let called = false; + await page.route(`${origin}/auth/v1/user`, async route => { + called = true; + await route.fulfill({ status: 500, body: "Unexpected request" }); + }); + await page.goto(`${base}/reset-password#error=access_denied&error_description=Email%20link%20expired`); + await expect(page.getByRole("alert")).toHaveText("Email link expired"); + await expect(page.getByRole("button", { name: "Update password" })).toBeHidden(); + await expect(page).toHaveURL(`${base}/reset-password`); + expect(called).toBe(false); +}); diff --git a/supabase/functions/api/handler.test.mjs b/supabase/functions/api/handler.test.mjs index 27b2a295..11e65a6c 100644 --- a/supabase/functions/api/handler.test.mjs +++ b/supabase/functions/api/handler.test.mjs @@ -76,6 +76,107 @@ test('confirmation page claims success only after an Auth redirect', async () => ['https://one-concept.supabase.co/functions/v1/api/reset-password#access_token=test-token&type=recovery']); }); +test('password reset page rejects invalid links and validates passwords before calling Auth', async () => { + const handler = createHandler(config); + const body = await (await handler(request('/reset-password'))).text(); + const script = body.match(/`; From b90b5d5853a16a85dd01196793255615ee3527cb Mon Sep 17 00:00:00 2001 From: Muawiya Amir Date: Wed, 28 Oct 2026 19:16:22 +0500 Subject: [PATCH 14/15] Keep released confirmation page neutral until email redirect --- admin/public/confirmed.js | 25 ++++++++++++++++++++++--- admin/public/confirmed/index.html | 6 +++--- admin/tests/public-pages.spec.ts | 29 +++++++++++++++++++++++++++++ docs/WORK_LOG.md | 1 + 4 files changed, 55 insertions(+), 6 deletions(-) diff --git a/admin/public/confirmed.js b/admin/public/confirmed.js index b784aef9..c2cccb96 100644 --- a/admin/public/confirmed.js +++ b/admin/public/confirmed.js @@ -1,3 +1,22 @@ -// Supabase may return a short-lived session in the URL fragment after -// confirmation. This informational page never needs it. -if (location.hash || location.search) history.replaceState(null, '', location.pathname); +// Supabase verifies the link before redirecting here. The page must not claim +// success on a direct visit, and it never needs to retain a returned session. +const hash = new URLSearchParams(location.hash.slice(1)); +const query = new URLSearchParams(location.search); +const error = hash.get('error_description') || query.get('error_description') || + hash.get('error') || query.get('error'); +if (hash.get('type') === 'recovery' && hash.get('access_token') && !error) { + const reset = new URL('/reset-password', location.origin); + reset.hash = location.hash; + location.replace(reset.toString()); +} else { + history.replaceState(null, '', location.pathname); + const heading = document.getElementById('heading'); + const message = document.getElementById('message'); + if (error) { + heading.textContent = 'Confirmation did not finish'; + message.textContent = error; + } else if (hash.get('access_token')) { + heading.textContent = 'Email confirmed ✓'; + message.textContent = "You're all set. Open the One Concept app and sign in to get today's concept."; + } +} diff --git a/admin/public/confirmed/index.html b/admin/public/confirmed/index.html index 55b83f85..f2760ab5 100644 --- a/admin/public/confirmed/index.html +++ b/admin/public/confirmed/index.html @@ -3,14 +3,14 @@ - One Concept — email confirmed + One Concept — email confirmation
-

Email confirmed

-

Open the One Concept app on your phone and sign in to get today's concept.

+

Check your email link

+

Open the confirmation link from your email to finish setting up your account.

diff --git a/admin/tests/public-pages.spec.ts b/admin/tests/public-pages.spec.ts index 513a42f8..9a46faba 100644 --- a/admin/tests/public-pages.spec.ts +++ b/admin/tests/public-pages.spec.ts @@ -25,6 +25,35 @@ test("review privacy policy opens the published learner privacy page", async ({ await expect(page.getByRole("heading", { name: "Privacy Policy" })).toBeVisible(); }); +test("Netlify confirmation stays neutral until an Auth redirect and clears its session", async ({ page }) => { + const html = await readFile(resolve("public/confirmed/index.html"), "utf8"); + await page.route("**/confirmed", route => route.fulfill({ contentType: "text/html", body: html })); + await page.goto("/confirmed"); + await expect(page.getByRole("heading", { name: "Check your email link" })).toBeVisible(); + await expect(page).toHaveTitle("One Concept — email confirmation"); + + await page.goto("/"); + await page.goto("/confirmed#access_token=confirmed-token&type=signup"); + await expect(page.getByRole("heading", { name: "Email confirmed ✓" })).toBeVisible(); + await expect(page).toHaveURL(/\/confirmed$/); + + await page.goto("/"); + await page.goto("/confirmed#error=access_denied&error_description=Email%20link%20expired"); + await expect(page.getByRole("heading", { name: "Confirmation did not finish" })).toBeVisible(); + await expect(page.locator("#message")).toHaveText("Email link expired"); + await expect(page).toHaveURL(/\/confirmed$/); +}); + +test("Netlify confirmation forwards a recovery session to its reset form", async ({ page }) => { + const html = await readFile(resolve("public/confirmed/index.html"), "utf8"); + const reset = await readFile(resolve("public/reset-password/index.html"), "utf8"); + await page.route("**/confirmed", route => route.fulfill({ contentType: "text/html", body: html })); + await page.route("**/reset-password", route => route.fulfill({ contentType: "text/html", body: reset })); + await page.goto("/confirmed#access_token=recovery-token&type=recovery"); + await expect(page).toHaveURL(/\/reset-password$/); + await expect(page.locator("#reset-form")).toBeVisible(); +}); + test("shared profile reads only public JSON and hides a revoked profile", async ({ page }) => { const html = await readFile(resolve("public/p/index.html"), "utf8"); await page.route(`**/p/${token}`, (route) => route.fulfill({ contentType: "text/html", body: html })); diff --git a/docs/WORK_LOG.md b/docs/WORK_LOG.md index 51e92548..da92c326 100644 --- a/docs/WORK_LOG.md +++ b/docs/WORK_LOG.md @@ -14,6 +14,7 @@ claims as completed work. - `91ce356` gives direct confirmation visits a neutral tab title, marks public Edge pages noindex, and adds Chromium success/expiry tests. The focused Chromium tests (3), 97 Edge handler tests, Edge TypeScript check, and admin TypeScript check passed locally. The email-template runbook now describes the Netlify origin used by the current production app. - Hosted [PR quality run 40701939325](https://github.com/Coding-Moves/one-concept/actions/runs/40701939325) passed mobile, backend, and review-site browser/build jobs on `8c90c71`; migration and Netlify checks passed or were intentionally skipped. A real inbox round trip still waits for the protected production Edge deployment after an approved merge. - A further reset-page pass found that the Edge page followed Auth redirects with the recovery bearer token, unlike the existing Netlify page. The page now rejects redirects, omits ambient credentials and caching, and shows a safe retry message for transport failures while retaining Auth's explicit error messages. A Chromium redirect regression, all six focused auth-page browser tests, 97 Edge handler tests, and Edge/admin TypeScript checks passed locally; hosted checks for this follow-up remain pending. +- The released production mobile app still uses Netlify confirmation links. That page also displayed a success heading on a direct visit, so it now starts neutral, reports Auth errors, and forwards a fallback recovery session to the existing reset form. Two new Chromium tests cover direct, success, expired, and recovery visits. All 59 review-site Chromium tests and admin TypeScript passed locally; hosted checks are pending for this Netlify follow-up. - `9dfd2eb` makes a direct `/confirmed` visit neutral instead of falsely reporting verified email, matching the correction on `develop`. `32fd36f` adds script-level recovery edge cases and Chromium tests for direct confirmation, a successful recovery update, and an expired link. - `922e77e` replaces the retired-host redirect runbook with the current `main` mobile redirect and Edge fallback sequence, and maps the Edge page source. Local validation: 97 Edge handler tests, Supabase function TypeScript check, 17 admin unit tests, admin build with CI fixture variables, and all 53 Chromium tests passed. Hosted PR checks and a real inbox recovery test remain to be verified after push; no migration is involved. - Live HTTP checks found the existing Netlify `/privacy` page returns 200 while the new production Edge `/privacy` page still returns 404 before deployment. `1e3eb2d` keeps the reviewer policy link on the working Netlify page and adds a focused Chromium navigation test, which passed. This removes a link outage between a future merge and the protected Edge deployment; #459 is already merged to `develop` for a later release. From 8783e8660d8b13304d20600b8c1bc04f24b309f0 Mon Sep 17 00:00:00 2001 From: Muawiya Amir Date: Wed, 28 Oct 2026 19:20:57 +0500 Subject: [PATCH 15/15] Record final auth landing review validation --- docs/WORK_LOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/WORK_LOG.md b/docs/WORK_LOG.md index da92c326..aa1cd2cb 100644 --- a/docs/WORK_LOG.md +++ b/docs/WORK_LOG.md @@ -13,8 +13,8 @@ claims as completed work. - Follow-up review scope: recheck the public browser output, hosted gateway configuration, and redirect documentation against the current `main` mobile app. The Edge API has `verify_jwt = false` in `supabase/config.toml`, so anonymous email-link navigation reaches the handler; private routes still verify account tokens. The direct confirmation page tab title and the email-template guide needed correction, and browser coverage lacked confirmation success/expiry cases. - `91ce356` gives direct confirmation visits a neutral tab title, marks public Edge pages noindex, and adds Chromium success/expiry tests. The focused Chromium tests (3), 97 Edge handler tests, Edge TypeScript check, and admin TypeScript check passed locally. The email-template runbook now describes the Netlify origin used by the current production app. - Hosted [PR quality run 40701939325](https://github.com/Coding-Moves/one-concept/actions/runs/40701939325) passed mobile, backend, and review-site browser/build jobs on `8c90c71`; migration and Netlify checks passed or were intentionally skipped. A real inbox round trip still waits for the protected production Edge deployment after an approved merge. -- A further reset-page pass found that the Edge page followed Auth redirects with the recovery bearer token, unlike the existing Netlify page. The page now rejects redirects, omits ambient credentials and caching, and shows a safe retry message for transport failures while retaining Auth's explicit error messages. A Chromium redirect regression, all six focused auth-page browser tests, 97 Edge handler tests, and Edge/admin TypeScript checks passed locally; hosted checks for this follow-up remain pending. -- The released production mobile app still uses Netlify confirmation links. That page also displayed a success heading on a direct visit, so it now starts neutral, reports Auth errors, and forwards a fallback recovery session to the existing reset form. Two new Chromium tests cover direct, success, expired, and recovery visits. All 59 review-site Chromium tests and admin TypeScript passed locally; hosted checks are pending for this Netlify follow-up. +- `7ec6753` rejects Auth redirects and ambient browser credentials on the Edge reset page, closing a recovery-token forwarding gap. A Chromium redirect regression, all six focused auth-page browser tests, 97 Edge handler tests, and Edge/admin TypeScript checks passed locally. Hosted [PR quality run 40703478981](https://github.com/Coding-Moves/one-concept/actions/runs/40703478981) passed mobile, backend, and review-site browser/build jobs. +- `b90b5d5` corrects the released Netlify confirmation page, which displayed a success heading on direct visits. It now starts neutral, reports Auth errors, and forwards a fallback recovery session to the existing reset form. Two new Chromium tests cover direct, success, expired, and recovery visits. All 59 review-site Chromium tests and admin TypeScript passed locally. Hosted [PR quality run 40704388844](https://github.com/Coding-Moves/one-concept/actions/runs/40704388844) passed mobile, backend, and review-site browser/build jobs; migration and Netlify checks passed or were appropriately skipped. A real inbox round trip remains after protected production deployment. - `9dfd2eb` makes a direct `/confirmed` visit neutral instead of falsely reporting verified email, matching the correction on `develop`. `32fd36f` adds script-level recovery edge cases and Chromium tests for direct confirmation, a successful recovery update, and an expired link. - `922e77e` replaces the retired-host redirect runbook with the current `main` mobile redirect and Edge fallback sequence, and maps the Edge page source. Local validation: 97 Edge handler tests, Supabase function TypeScript check, 17 admin unit tests, admin build with CI fixture variables, and all 53 Chromium tests passed. Hosted PR checks and a real inbox recovery test remain to be verified after push; no migration is involved. - Live HTTP checks found the existing Netlify `/privacy` page returns 200 while the new production Edge `/privacy` page still returns 404 before deployment. `1e3eb2d` keeps the reviewer policy link on the working Netlify page and adds a focused Chromium navigation test, which passed. This removes a link outage between a future merge and the protected Edge deployment; #459 is already merged to `develop` for a later release.