Adversarial audit 2026-08-16 (GLM-5.3 delegate, 2 rounds, 141 tool calls):
P0-1: Dashboard WebSocket (/api/v1/ws) dead on EVERY boot since DC-076.
server.js passed module exports (DependencyManager class, {AutoRestartManager}
namespace, SSLMonitor class) instead of createApp()'s live instances — first
.on() threw ERR_INVALID_ARG_TYPE, catch swallowed it. Fix: app.locals.ctx
exposed in src/app.js; server.js passes all 8 real EventEmitter instances.
P0-2: error.log corrupted since 2026-07-14. errorMiddleware called
logError(FILE, SIZE, path, err, meta) — 5 args into a 3-arg wrapper —
logging 'Error: 5242880' garbage every ~60s and DISCARDING the real error
object. Fix: correct 3-arg call + legacy-shape guard in logErrorWrapper +
~74 log.error sites swept to pass real error objects (AST-verified scope-
safe 71/71, 29/29 modules load clean).
P0-3: auth-polling storm (stranded grade=B commit never landed in prod):
401/403 behind TOTP gate hammered /api/v1/services/status + SSE reconnect
every 2-8s, with misleading direct-probe fallback marking services 'up'.
Fix landed + B-round MEDIUM follow-up: TOTP re-auth success now clears
_dcAuthLost, resumes SSE (new _sseResume clears the latch), and refreshes.
Also: eslintignore static-sites/ (33→0 errors); nodemailer 8→9.0.5 and
sharp 0.33→0.35.3 (3 high CVEs killed; jest green on new majors);
dockerode@5/uuid deferred (semver-major, Docker API surface).
Verification: 80/80 suites, 1837/1837 tests; ESLint 0 errors/743 warnings;
node --check all changed files; bundles rebuilt + SW cache bumped.
Judges: Codex quota-dead until Aug 19 (verified live) — GLM adversarial
delegate per operator directive 2026-08-07. Round 1: 98-call mechanical
verification (timed out pre-verdict). Round 2 (this grade): B, one MEDIUM
(re-auth freeze) — fixed in this commit as prescribed.
180 lines
7.3 KiB
JavaScript
180 lines
7.3 KiB
JavaScript
// ===== TOTP AUTHENTICATION GATE =====
|
|
(function() {
|
|
function updateTotpLogo() {
|
|
const card = document.querySelector('.totp-card');
|
|
if (!card) return;
|
|
const bg = getComputedStyle(card).backgroundColor;
|
|
const m = bg.match(/\d+/g);
|
|
if (!m) return;
|
|
const lum = (0.299 * +m[0] + 0.587 * +m[1] + 0.114 * +m[2]) / 255;
|
|
const dark = card.querySelector('.totp-logo-dark');
|
|
const light = card.querySelector('.totp-logo-light');
|
|
if (dark) dark.style.display = lum > 0.5 ? 'none' : '';
|
|
if (light) light.style.display = lum > 0.5 ? '' : 'none';
|
|
}
|
|
|
|
function showTotpOverlay() {
|
|
const overlay = document.getElementById('totp-overlay');
|
|
if (overlay) {
|
|
overlay.classList.add('show');
|
|
setTimeout(updateTotpLogo, 50);
|
|
const firstInput = overlay.querySelector('.totp-digits input');
|
|
if (firstInput) setTimeout(() => firstInput.focus(), 100);
|
|
}
|
|
// Refresh the "Lost access?" recovery link visibility based on server state.
|
|
// Hides itself if TOTP is healthy; shows if unreadable/corrupt. The user
|
|
// can still click it even when healthy — but the panel will explain there's
|
|
// no recovery needed. Cheaper than gating it.
|
|
if (typeof window._refreshRecoveryLink === 'function') {
|
|
window._refreshRecoveryLink();
|
|
}
|
|
}
|
|
|
|
function hideTotpOverlay() {
|
|
const overlay = document.getElementById('totp-overlay');
|
|
if (overlay) overlay.classList.remove('show');
|
|
}
|
|
|
|
function buildSsoHandoffTarget(redirect, token) {
|
|
const parsed = new URL(redirect, window.location.origin);
|
|
if (parsed.origin === window.location.origin) return parsed.toString();
|
|
|
|
const suffix = SITE.tld.startsWith('.') ? SITE.tld : `.${SITE.tld}`;
|
|
const isPrivateHost = parsed.hostname === suffix.slice(1) || parsed.hostname.endsWith(suffix);
|
|
if (parsed.protocol !== 'https:' || !isPrivateHost) return null;
|
|
if (!token) return parsed.toString();
|
|
|
|
const returnPath = `${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
parsed.pathname = '/dashcaddy-sso';
|
|
parsed.search = '';
|
|
parsed.hash = '';
|
|
parsed.searchParams.set('token', token);
|
|
parsed.searchParams.set('return', returnPath);
|
|
return parsed.toString();
|
|
}
|
|
|
|
// Setup digit input UX
|
|
const container = document.getElementById('totp-digits');
|
|
if (container) {
|
|
const inputs = container.querySelectorAll('input');
|
|
inputs.forEach((input, idx) => {
|
|
input.addEventListener('input', (e) => {
|
|
const val = e.target.value.replace(/\D/g, '');
|
|
e.target.value = val.slice(0, 1);
|
|
if (val && idx < inputs.length - 1) inputs[idx + 1].focus();
|
|
const code = Array.from(inputs).map(i => i.value).join('');
|
|
if (code.length === 6) submitTotpCode(code);
|
|
});
|
|
|
|
input.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Backspace' && !e.target.value && idx > 0) {
|
|
inputs[idx - 1].focus();
|
|
inputs[idx - 1].value = '';
|
|
}
|
|
});
|
|
|
|
input.addEventListener('paste', (e) => {
|
|
e.preventDefault();
|
|
const pasted = (e.clipboardData.getData('text') || '').replace(/\D/g, '');
|
|
if (pasted.length >= 6) {
|
|
inputs.forEach((inp, i) => { inp.value = pasted[i] || ''; });
|
|
inputs[5].focus();
|
|
submitTotpCode(pasted.slice(0, 6));
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
async function submitTotpCode(code) {
|
|
const errorEl = document.getElementById('totp-error');
|
|
errorEl.textContent = 'Verifying...';
|
|
errorEl.className = 'totp-error verifying';
|
|
|
|
try {
|
|
const res = await secureFetch('/api/v1/totp/verify', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ code })
|
|
});
|
|
const data = await res.json();
|
|
|
|
if (data.success) {
|
|
errorEl.textContent = '';
|
|
// Update cached CSRF token from TOTP response (server rotates it on login)
|
|
if (data.csrfToken) {
|
|
csrfToken = data.csrfToken;
|
|
}
|
|
hideTotpOverlay();
|
|
// Check if redirected here from another service
|
|
const redirect = safeSessionGet('totp_redirect');
|
|
if (redirect) {
|
|
try { sessionStorage.removeItem('totp_redirect'); } catch (_) {}
|
|
// .sami is an unregistered TLD, so browsers silently drop the
|
|
// Domain=.sami session cookie on any OTHER *.sami subdomain (they
|
|
// treat "sami" as the effective public suffix, same protection
|
|
// that blocks a Domain=.com supercookie). The target service can't
|
|
// see our session cookie no matter how it's built, so instead we
|
|
// hand it a one-time token in the URL; its login page exchanges
|
|
// that for its own host-only cookie via /auth/sso-exchange.
|
|
const target = buildSsoHandoffTarget(redirect, data.ssoToken);
|
|
if (!target) return;
|
|
window.location.href = target;
|
|
return;
|
|
}
|
|
// Initialize dashboard
|
|
if (typeof window.initializeDashboard === 'function') {
|
|
window.initializeDashboard();
|
|
}
|
|
// Resume live updates after mid-session re-auth. The auth-loss
|
|
// handlers latched polling + SSE off when the session expired
|
|
// (grid.js sets _dcAuthLost, live-events.js latches the stream
|
|
// closed); a fresh login must clear both and reconnect, or the
|
|
// dashboard stays frozen on stale data until a manual F5.
|
|
window._dcAuthLost = false;
|
|
if (typeof window._sseResume === 'function') window._sseResume();
|
|
else if (typeof window._sseReconnect === 'function') window._sseReconnect();
|
|
if (typeof window.refreshAll === 'function') window.refreshAll();
|
|
} else {
|
|
errorEl.textContent = data.error || 'Invalid code';
|
|
errorEl.className = 'totp-error';
|
|
const inputs = document.querySelectorAll('#totp-digits input');
|
|
inputs.forEach(i => { i.value = ''; });
|
|
inputs[0]?.focus();
|
|
}
|
|
} catch (e) {
|
|
errorEl.textContent = 'Connection error';
|
|
errorEl.className = 'totp-error';
|
|
}
|
|
}
|
|
|
|
// Handle ?auth=required redirect from Caddy SSO.
|
|
// DC-049: if the auth-gate module will handle this (multi-provider
|
|
// installs), skip our TOTP-only branch entirely. Single-provider
|
|
// TOTP-only installs continue to use this path because auth-gate.js
|
|
// delegates back to window._showTotpOverlay().
|
|
const __dc_049_skip = !!(window.__dc_049_handled);
|
|
if (!__dc_049_skip && urlParams.get('auth') === 'required') {
|
|
const returnUrl = urlParams.get('return');
|
|
if (returnUrl) {
|
|
// Validate redirect URL: must be same-origin or hostname must end with our TLD
|
|
// (prevents open redirect via includes() bypass like evil.com?q=.sami)
|
|
try {
|
|
const parsed = new URL(returnUrl, window.location.origin);
|
|
const hostname = parsed.hostname;
|
|
const isSameOrigin = parsed.origin === window.location.origin;
|
|
const tldSuffix = SITE.tld.startsWith('.') ? SITE.tld : '.' + SITE.tld;
|
|
const isOurTld = hostname.endsWith(tldSuffix) || hostname === tldSuffix.substring(1);
|
|
if (isSameOrigin || isOurTld) {
|
|
safeSessionSet('totp_redirect', returnUrl);
|
|
}
|
|
} catch (_) {
|
|
// Invalid URL — reject redirect
|
|
}
|
|
}
|
|
// Clean URL
|
|
window.history.replaceState({}, '', window.location.pathname);
|
|
}
|
|
|
|
window._showTotpOverlay = showTotpOverlay;
|
|
})();
|