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
+88 -49
View File
@@ -290,16 +290,20 @@ describe('POST /api/me/password — self-change password (AUTH-LOCAL-09)', () =>
where: vi.fn().mockReturnValue({
limit: vi.fn().mockResolvedValue([{ passwordHash: storedHash, userId: 1 }]),
}),
innerJoin: vi.fn().mockReturnValue({ innerJoin: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue([]) }) }),
innerJoin: vi.fn().mockReturnValue({
innerJoin: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue([]) }),
}),
}),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any;
}
// fallback for other selects
return {
from: vi.fn().mockReturnValue({
where: vi.fn().mockReturnValue({ limit: vi.fn().mockResolvedValue([]) }),
innerJoin: vi.fn().mockReturnValue({ innerJoin: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue([]) }) }),
innerJoin: vi.fn().mockReturnValue({
innerJoin: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue([]) }),
}),
}),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any;
@@ -346,7 +350,9 @@ describe('POST /api/me/password — self-change password (AUTH-LOCAL-09)', () =>
where: vi.fn().mockReturnValue({
limit: vi.fn().mockResolvedValue([{ passwordHash: storedHash, userId: 1 }]),
}),
innerJoin: vi.fn().mockReturnValue({ innerJoin: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue([]) }) }),
innerJoin: vi.fn().mockReturnValue({
innerJoin: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue([]) }),
}),
}),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any;
@@ -354,7 +360,9 @@ describe('POST /api/me/password — self-change password (AUTH-LOCAL-09)', () =>
return {
from: vi.fn().mockReturnValue({
where: vi.fn().mockReturnValue({ limit: vi.fn().mockResolvedValue([]) }),
innerJoin: vi.fn().mockReturnValue({ innerJoin: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue([]) }) }),
innerJoin: vi.fn().mockReturnValue({
innerJoin: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue([]) }),
}),
}),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any;
@@ -386,13 +394,18 @@ describe('POST /api/me/password — self-change password (AUTH-LOCAL-09)', () =>
it('Test 3: user with no local_credentials row → 404', async () => {
const { db } = await import('../../src/db/client.js');
vi.mocked(db.select).mockImplementation(() => ({
from: vi.fn().mockReturnValue({
where: vi.fn().mockReturnValue({ limit: vi.fn().mockResolvedValue([]) }),
innerJoin: vi.fn().mockReturnValue({ innerJoin: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue([]) }) }),
}),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any));
vi.mocked(db.select).mockImplementation(
() =>
({
from: vi.fn().mockReturnValue({
where: vi.fn().mockReturnValue({ limit: vi.fn().mockResolvedValue([]) }),
innerJoin: vi.fn().mockReturnValue({
innerJoin: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue([]) }),
}),
}),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}) as any,
);
const { app } = await import('../../src/index.js');
const res = await app.request('/api/me/password', {
@@ -457,7 +470,9 @@ describe('GET /api/me — hasLocalCredential (AUTH-LOCAL-17)', () => {
return {
from: vi.fn().mockReturnValue({
where: vi.fn().mockReturnValue({ limit: vi.fn().mockResolvedValue(limitResult) }),
innerJoin: vi.fn().mockReturnValue({ innerJoin: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue([]) }) }),
innerJoin: vi.fn().mockReturnValue({
innerJoin: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue([]) }),
}),
}),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any;
@@ -483,7 +498,9 @@ describe('GET /api/me — hasLocalCredential (AUTH-LOCAL-17)', () => {
return {
from: vi.fn().mockReturnValue({
where: vi.fn().mockReturnValue({ limit: vi.fn().mockResolvedValue(limitResult) }),
innerJoin: vi.fn().mockReturnValue({ innerJoin: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue([]) }) }),
innerJoin: vi.fn().mockReturnValue({
innerJoin: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue([]) }),
}),
}),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any;
@@ -520,14 +537,14 @@ describe('linkOidcToUser helper (AUTH-LOCAL-10, T-19-08)', () => {
let deletedLocalCreds = false;
// Mock: SELECT users WHERE oidc_iss = iss AND oidc_sub = sub → no conflict (empty)
let txSelectCount = 0;
const mockTx = {
select: vi.fn().mockImplementation(() => {
txSelectCount++;
return {
from: vi.fn().mockReturnValue({
where: vi.fn().mockReturnValue({ limit: vi.fn().mockResolvedValue([]) }), // no conflict
innerJoin: vi.fn().mockReturnValue({ innerJoin: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue([]) }) }),
innerJoin: vi.fn().mockReturnValue({
innerJoin: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue([]) }),
}),
}),
};
}),
@@ -545,18 +562,25 @@ describe('linkOidcToUser helper (AUTH-LOCAL-10, T-19-08)', () => {
})),
};
vi.mocked(db.select).mockImplementation(() => ({
from: vi.fn().mockReturnValue({
where: vi.fn().mockReturnValue({ limit: vi.fn().mockResolvedValue([]) }), // preflight: no conflict
innerJoin: vi.fn().mockReturnValue({ innerJoin: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue([]) }) }),
}),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any));
vi.mocked(db.select).mockImplementation(
() =>
({
from: vi.fn().mockReturnValue({
where: vi.fn().mockReturnValue({ limit: vi.fn().mockResolvedValue([]) }), // preflight: no conflict
innerJoin: vi.fn().mockReturnValue({
innerJoin: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue([]) }),
}),
}),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}) as any,
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
vi.mocked(db).transaction = vi.fn().mockImplementation(async (fn: (tx: any) => Promise<void>) => {
await fn(mockTx);
});
vi.mocked(db).transaction = vi
.fn()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.mockImplementation(async (fn: (tx: any) => Promise<void>) => {
await fn(mockTx);
});
await linkOidcToUser(42, iss, sub);
@@ -574,15 +598,20 @@ describe('linkOidcToUser helper (AUTH-LOCAL-10, T-19-08)', () => {
let deletedLocalCreds = false;
// Preflight SELECT finds a conflicting user (id=99, different from target=42)
vi.mocked(db.select).mockImplementation(() => ({
from: vi.fn().mockReturnValue({
where: vi.fn().mockReturnValue({
limit: vi.fn().mockResolvedValue([{ id: conflictingUserId }]), // conflict!
}),
innerJoin: vi.fn().mockReturnValue({ innerJoin: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue([]) }) }),
}),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any));
vi.mocked(db.select).mockImplementation(
() =>
({
from: vi.fn().mockReturnValue({
where: vi.fn().mockReturnValue({
limit: vi.fn().mockResolvedValue([{ id: conflictingUserId }]), // conflict!
}),
innerJoin: vi.fn().mockReturnValue({
innerJoin: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue([]) }),
}),
}),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}) as any,
);
// Transaction should NEVER be called on conflict
const mockTx = {
@@ -593,10 +622,12 @@ describe('linkOidcToUser helper (AUTH-LOCAL-10, T-19-08)', () => {
}),
})),
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
vi.mocked(db).transaction = vi.fn().mockImplementation(async (fn: (tx: any) => Promise<void>) => {
await fn(mockTx);
});
vi.mocked(db).transaction = vi
.fn()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.mockImplementation(async (fn: (tx: any) => Promise<void>) => {
await fn(mockTx);
});
// Should throw OidcLinkConflictError, not proceed to transaction
await expect(linkOidcToUser(42, iss, sub)).rejects.toThrow(OidcLinkConflictError);
@@ -611,13 +642,20 @@ describe('linkOidcToUser helper (AUTH-LOCAL-10, T-19-08)', () => {
const { db } = await import('../../src/db/client.js');
// Mock db — not needed for route shape test but avoids errors
vi.mocked(db.select).mockImplementation(() => ({
from: vi.fn().mockReturnValue({
where: vi.fn().mockReturnValue({ limit: vi.fn().mockResolvedValue([{ isAdmin: false }]) }),
innerJoin: vi.fn().mockReturnValue({ innerJoin: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue([]) }) }),
}),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any));
vi.mocked(db.select).mockImplementation(
() =>
({
from: vi.fn().mockReturnValue({
where: vi
.fn()
.mockReturnValue({ limit: vi.fn().mockResolvedValue([{ isAdmin: false }]) }),
innerJoin: vi.fn().mockReturnValue({
innerJoin: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue([]) }),
}),
}),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}) as any,
);
const { app } = await import('../../src/index.js');
const res = await app.request('/api/me/link-oidc', {
@@ -630,7 +668,8 @@ describe('linkOidcToUser helper (AUTH-LOCAL-10, T-19-08)', () => {
const body = (await res.json()) as { authorizationUrl?: string; state?: string };
// The response must have at minimum a signedState field (or authorizationUrl)
// — the exact shape depends on implementation; assert it's an object with a useful field
const hasInitiationPayload = 'authorizationUrl' in body || 'state' in body || 'signedState' in body;
const hasInitiationPayload =
'authorizationUrl' in body || 'state' in body || 'signedState' in body;
expect(hasInitiationPayload).toBe(true);
});
});