fix: merge hooks.ts into hooks.server.ts — root cause of K.resolve hydration crash
hooks.ts contained Node-only imports (fs, path) and was being included in
the client bundle by SvelteKit because it lacked the .server. designation.
This caused 'K.resolve is not a function' in the browser, which aborted
hydration and killed ALL client-side interactivity site-wide.
Fix: merge reroute export from hooks.ts into hooks.server.ts alongside the
existing CSRF handle. Delete hooks.ts. SvelteKit supports both handle and
reroute exports from hooks.server.ts.
This was the true root cause of today's outage. The CSS and issues
were red herrings — the site was broken from commit 231eacc onward.
This commit is contained in:
@@ -1,9 +1,61 @@
|
||||
import { type Handle } from '@sveltejs/kit';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { type Handle, type Reroute } from '@sveltejs/kit';
|
||||
import { env } from '$env/dynamic/private';
|
||||
|
||||
// ── Admin path rerouting ────────────────────────────────────────────────────
|
||||
|
||||
const CONFIG_PATH = path.resolve('data', 'config.json');
|
||||
|
||||
// Cache adminPath with a 5-second TTL so changes take effect quickly
|
||||
// but we don't hit disk on every request.
|
||||
let cachedAdminPath = 'admin';
|
||||
let cacheExpiry = 0;
|
||||
|
||||
function getAdminPath(): string {
|
||||
const now = Date.now();
|
||||
if (now < cacheExpiry) return cachedAdminPath;
|
||||
|
||||
try {
|
||||
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
||||
cachedAdminPath = (config.adminPath ?? 'admin').replace(/^\/|\/$/g, '') || 'admin';
|
||||
} catch {
|
||||
// config.json missing or unreadable — keep current cached value
|
||||
}
|
||||
|
||||
cacheExpiry = now + 5000;
|
||||
return cachedAdminPath;
|
||||
}
|
||||
|
||||
export { getAdminPath as adminPath };
|
||||
|
||||
/**
|
||||
* Rewrites /<adminPath>/... to /admin/... before SvelteKit resolves the route.
|
||||
* When adminPath is not 'admin', direct access to /admin is rewritten to /404
|
||||
* so attackers cannot enumerate the admin URL.
|
||||
*/
|
||||
export const reroute: Reroute = ({ url }) => {
|
||||
const adminPath = getAdminPath();
|
||||
const prefix = `/${adminPath}`;
|
||||
|
||||
// Public path → internal /admin
|
||||
if (url.pathname === prefix || url.pathname.startsWith(prefix + '/')) {
|
||||
return '/admin' + url.pathname.slice(prefix.length);
|
||||
}
|
||||
|
||||
// Block direct /admin access when a custom path is configured
|
||||
if (adminPath !== 'admin') {
|
||||
if (url.pathname === '/admin' || url.pathname.startsWith('/admin/')) {
|
||||
return '/404';
|
||||
}
|
||||
}
|
||||
|
||||
return url.pathname;
|
||||
};
|
||||
|
||||
// ── CSRF origin allowlist ───────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* CSRF origin allowlist.
|
||||
*
|
||||
* Production: set ALLOWED_ORIGINS to a comma-separated list of trusted origins.
|
||||
* e.g. ALLOWED_ORIGINS=https://humorunits.com,https://dickloads.com
|
||||
*
|
||||
|
||||
51
src/hooks.ts
51
src/hooks.ts
@@ -1,51 +0,0 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import type { Reroute } from '@sveltejs/kit';
|
||||
|
||||
const CONFIG_PATH = path.resolve('data', 'config.json');
|
||||
|
||||
// Cache adminPath with a 5-second TTL so changes take effect quickly
|
||||
// but we don't hit disk on every request.
|
||||
let cachedAdminPath = 'admin';
|
||||
let cacheExpiry = 0;
|
||||
|
||||
function getAdminPath(): string {
|
||||
const now = Date.now();
|
||||
if (now < cacheExpiry) return cachedAdminPath;
|
||||
|
||||
try {
|
||||
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
||||
cachedAdminPath = (config.adminPath ?? 'admin').replace(/^\/|\/$/g, '') || 'admin';
|
||||
} catch {
|
||||
// config.json missing or unreadable — keep current cached value
|
||||
}
|
||||
|
||||
cacheExpiry = now + 5000;
|
||||
return cachedAdminPath;
|
||||
}
|
||||
|
||||
export { getAdminPath as adminPath };
|
||||
|
||||
/**
|
||||
* Rewrites /<adminPath>/... to /admin/... before SvelteKit resolves the route.
|
||||
* When adminPath is not 'admin', direct access to /admin is rewritten to /404
|
||||
* so attackers cannot enumerate the admin URL.
|
||||
*/
|
||||
export const reroute: Reroute = ({ url }) => {
|
||||
const adminPath = getAdminPath();
|
||||
const prefix = `/${adminPath}`;
|
||||
|
||||
// Public path → internal /admin
|
||||
if (url.pathname === prefix || url.pathname.startsWith(prefix + '/')) {
|
||||
return '/admin' + url.pathname.slice(prefix.length);
|
||||
}
|
||||
|
||||
// Block direct /admin access when a custom path is configured
|
||||
if (adminPath !== 'admin') {
|
||||
if (url.pathname === '/admin' || url.pathname.startsWith('/admin/')) {
|
||||
return '/404';
|
||||
}
|
||||
}
|
||||
|
||||
return url.pathname;
|
||||
};
|
||||
Reference in New Issue
Block a user