// ===== ENCRYPTED VAULT -> SERVICE SSO HANDOFF ===== (function() { function isAllowedReturnUrl(returnUrl, expectedServiceId) { if (!returnUrl || !expectedServiceId || !/^[a-z0-9][a-z0-9-]*$/.test(expectedServiceId)) return false; try { const parsed = new URL(returnUrl, window.location.origin); const suffix = SITE.tld.startsWith('.') ? SITE.tld : `.${SITE.tld}`; const expectedHost = `${expectedServiceId}${suffix}`; return parsed.protocol === 'https:' && parsed.hostname === expectedHost; } catch (_) { return false; } } function buildHandoffTarget(returnUrl, token, expectedServiceId) { if (!isAllowedReturnUrl(returnUrl, expectedServiceId)) return null; const parsed = new URL(returnUrl, window.location.origin); if (!token) return null; const returnPath = `${parsed.pathname}${parsed.search}${parsed.hash}`; // The shared (dashcaddy_auth) Caddy snippet installs this public landing // route on every protected host. It rewrites to /api/v1/auth/sso-exchange. parsed.pathname = '/dashcaddy-sso'; parsed.search = ''; parsed.hash = ''; parsed.searchParams.set('token', token); parsed.searchParams.set('return', returnPath); return parsed.toString(); } async function resume(returnUrl, expectedServiceId, runtime = {}) { if (!isAllowedReturnUrl(returnUrl, expectedServiceId)) return false; const fetchFn = runtime.fetch || window.fetch.bind(window); const locationObj = runtime.location || window.location; try { const response = await fetchFn(`/api/v1/auth/sso-handoff?serviceId=${encodeURIComponent(expectedServiceId)}`, { credentials: 'include', cache: 'no-store', }); if (!response.ok) return false; const data = await response.json(); const target = data.success && buildHandoffTarget(returnUrl, data.ssoToken, expectedServiceId); if (!target) return false; locationObj.replace(target); return true; } catch (_) { return false; } } window.DCCredentialVault = { isAllowedReturnUrl, buildHandoffTarget, resume }; })();