fix(19): satisfy CI fast-checks + secret scan
CI / changes (pull_request) Successful in 9s
CI / api (pull_request) Successful in 3m2s
CI / fast-checks (pull_request) Successful in 4m20s
CI / security (pull_request) Successful in 1m14s
CI / harness (pull_request) Successful in 6m56s
CI / gate (pull_request) Successful in 2s

Lint (eslint --max-warnings 0):
- index.ts: disable no-unsafe-argument on the type-only Context mismatch when
  delegating to the OIDC handler inside the local-session skip wrapper
- localAuth.ts: handleLogout is sync (no await) — drop async (require-await)
- devBypass.ts: disable detect-possible-timing-attacks on the public well-known
  dev-placeholder string compare (not a secret comparison)
- remove dead code / unused bindings flagged by no-unused-vars: makeTestApp
  (localSession.test), makeUnauthContext + BrowserContext import (login.spec),
  unused memberId (admin.test), unused txSelectCount counter (me.test)
- localAuthMiddleware.test / me.test: fix unused + reflow-detached
  eslint-disable directives

Format: prettier --write across the 20 Phase-19 files that were never formatted.

Secret scan (gitleaks): allowlist two false positives — the synthetic >=32-char
TEST_SECRET in localSession.test.ts, and .planning/ design prose (a generic-api-key
regex hit on "credential atomically, 409-equivalent"). Neither is a real secret.

Verified locally: format:check, lint, typecheck, md:lint, gitleaks (no leaks),
PWA 266/266, API 452/452.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lucas Berger
2026-06-17 23:05:15 -04:00
co-authored by Claude Opus 4.8
parent 91ab9d1f78
commit b6490feff4
22 changed files with 318 additions and 294 deletions
+5 -5
View File
@@ -195,10 +195,7 @@ export default function App() {
Phase 19: shown when the user is unauthenticated AND localEnabled === true.
The route itself always renders LoginPage (authMode gating is in the `*` route gate below).
LoginPage receives authMode so it can show the optional OIDC button when oidcEnabled. */}
<Route
path="/login"
element={<LoginPage authMode={authModeQuery.data} />}
/>
<Route path="/login" element={<LoginPage authMode={authModeQuery.data} />} />
{/* All other routes are gated on setup completion */}
<Route
@@ -213,7 +210,10 @@ export default function App() {
) : meQuery.isError && !meQuery.isLoading && authModeQuery.data?.localEnabled ? (
// Unauthenticated + localEnabled: redirect to /login
<Navigate to="/login" replace />
) : meQuery.isError && !meQuery.isLoading && !authModeQuery.data?.localEnabled && authModeQuery.data?.oidcEnabled ? (
) : meQuery.isError &&
!meQuery.isLoading &&
!authModeQuery.data?.localEnabled &&
authModeQuery.data?.oidcEnabled ? (
// Unauthenticated + OIDC-only mode: top-level redirect to /api/login (today's behavior)
// Use a render side-effect via useEffect isn't available here; use a helper element
<OidcRedirect />
+1 -4
View File
@@ -103,10 +103,7 @@ export async function fetchAuthMode(): Promise<{ localEnabled: boolean; oidcEnab
*
* Throws nothing on 200 OK — the local-session cookie is set by the server.
*/
export async function fetchLocalLogin(body: {
username: string;
password: string;
}): Promise<void> {
export async function fetchLocalLogin(body: { username: string; password: string }): Promise<void> {
const res = await fetch('/api/auth/local/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -28,7 +28,14 @@ vi.mock('../hooks/usePushSubscription.js', () => ({
// Mock client so the test doesn't make real network calls.
vi.mock('../api/client.js', () => ({
fetchMe: vi.fn().mockResolvedValue({
user: { id: 1, displayName: 'Test', color: '#4a90d9', isAdmin: false, needsProviderSetup: false, hasLocalCredential: false },
user: {
id: 1,
displayName: 'Test',
color: '#4a90d9',
isAdmin: false,
needsProviderSetup: false,
hasLocalCredential: false,
},
}),
fetchAuthMode: vi.fn().mockResolvedValue({ localEnabled: true, oidcEnabled: false }),
fetchChangePassword: vi.fn().mockResolvedValue(undefined),
+4 -5
View File
@@ -500,10 +500,7 @@ export function SettingsSheet({ isOpen, onClose }: SettingsSheetProps) {
{/* Surface 13 — Link OIDC confirmation sheet (hasLocalCredential + oidcEnabled gate) */}
{linkOidcOpen && (
<LinkOidcSheet
isOpen={linkOidcOpen}
onClose={() => setLinkOidcOpen(false)}
/>
<LinkOidcSheet isOpen={linkOidcOpen} onClose={() => setLinkOidcOpen(false)} />
)}
</>
);
@@ -935,7 +932,9 @@ function LinkOidcSheet({ isOpen, onClose }: LinkOidcSheetProps) {
color: 'var(--color-text-secondary, #6b7280)',
}}
>
{"After linking, you'll sign in with your OIDC provider instead of a username and password. Your local password will be removed."}
{
"After linking, you'll sign in with your OIDC provider instead of a username and password. Your local password will be removed."
}
</p>
{/* Secondary note */}
+1 -2
View File
@@ -1241,8 +1241,7 @@ function ResetPasswordSheet({ isOpen, onClose, member }: ResetPasswordSheetProps
});
const isPending = resetMutation.isPending;
const submitDisabled =
isPending || newPassword.length === 0 || confirmPassword.length === 0;
const submitDisabled = isPending || newPassword.length === 0 || confirmPassword.length === 0;
if (!isOpen) return null;
+1 -4
View File
@@ -146,10 +146,7 @@ export function LoginPage({ authMode }: LoginPageProps) {
const isLoading = loginMutation.isPending;
const bothNonEmpty = username.trim().length > 0 && password.length > 0;
const submitDisabled =
isLoading ||
!bothNonEmpty ||
loginError === 'rate-limit' ||
loginError === 'locked';
isLoading || !bothNonEmpty || loginError === 'rate-limit' || loginError === 'locked';
// Derive whether inputs should show error state
const inputHasError = loginError === 'invalid';
+5 -5
View File
@@ -94,11 +94,11 @@
* never the BrandSlot component structure (see 19-UI-SPEC.md §Brand Slot).
* ───────────────────────────────────────────────────────────────────────── */
--brand-logo-bg: var(--color-member-0); /* placeholder circle background */
--brand-logo-text: #ffffff; /* placeholder initials color */
--brand-logo-size: 48px; /* reserved slot height; keep 1:1 aspect */
--brand-logo-border-radius: 50%; /* circle for initials; Phase 17 may change */
--brand-app-name: 'FamilySync'; /* drives doc only — not used as CSS content */
--brand-logo-bg: var(--color-member-0); /* placeholder circle background */
--brand-logo-text: #ffffff; /* placeholder initials color */
--brand-logo-size: 48px; /* reserved slot height; keep 1:1 aspect */
--brand-logo-border-radius: 50%; /* circle for initials; Phase 17 may change */
--brand-app-name: 'FamilySync'; /* drives doc only — not used as CSS content */
/* ─────────────────────────────────────────────────────────────────────────
* BREAKPOINTS (reference; use in @media queries)