import { verifySession } from './auth/session.js';
import { isIpAllowed } from './security/ip-allowlist.js';
import { getClientIp, isResolvedIp } from './security/client-ip.js';
// Accept the full `ActuateCMSConfig` directly (type-only import — erased at
// build time) so consumers can pass their `actuate.config` export without an
// `as unknown as` cast, while still tolerating the looser shape used in tests.
export function createCMSMiddleware(cmsConfig) {
    const adminPath = cmsConfig.admin?.path ?? '/admin';
    // Split-login installs serve the auth screens on their own mount; it gets
    // the same guards (IP allowlist, CMS_SECRET check) as the admin itself.
    const loginPath = cmsConfig.admin?.loginPath;
    const ipAllowlist = cmsConfig.admin?.ipAllowlist;
    const secret = process.env.CMS_SECRET ?? '';
    return async function cmsMiddleware(request) {
        const url = new URL(request.url);
        const isAdminRoute = pathMatchesPrefix(url.pathname, adminPath) ||
            (loginPath ? pathMatchesPrefix(url.pathname, loginPath) : false);
        const isCMSApiRoute = pathMatchesPrefix(url.pathname, '/api/cms');
        if (!isAdminRoute && !isCMSApiRoute) {
            return null;
        }
        // Setup wizard endpoints must be reachable when there's no admin user yet
        // — they're disabled by the wizard handler itself once the first admin
        // exists. Both the UI route and the API route need to bypass IP/session
        // checks during initial install.
        const isSetupRoute = url.pathname === `${adminPath}/setup` ||
            url.pathname.startsWith(`${adminPath}/setup/`) ||
            (loginPath
                ? url.pathname === `${loginPath}/setup` || url.pathname.startsWith(`${loginPath}/setup/`)
                : false) ||
            url.pathname.startsWith('/api/cms/setup');
        if (isSetupRoute) {
            return null;
        }
        // IP allowlist check for admin AND admin-API routes. The original code
        // only checked admin UI routes, leaving the API endpoints reachable from
        // any IP — which defeats the entire point of an allowlist for an
        // attacker who already has a stolen cookie.
        if (ipAllowlist && ipAllowlist.length > 0 && (isAdminRoute || isCMSApiRoute)) {
            const clientIp = getClientIp(request);
            // 'unknown' is never on an allowlist — fail closed when we cannot
            // resolve a trustworthy source IP.
            if (!isResolvedIp(clientIp) || !isIpAllowed(clientIp, ipAllowlist)) {
                return new Response('Forbidden', { status: 403 });
            }
        }
        // CMS_SECRET is mandatory: it signs session JWTs, so the admin (including
        // the setup wizard, which auto-logs-in the new admin) cannot function
        // without it. Rather than a bare 500, return an actionable page that tells
        // the operator exactly what to set. The setup *API* route is already
        // bypassed above, so this only guards the admin UI.
        if (isAdminRoute && !secret) {
            return configMissingResponse();
        }
        // Session verification for admin routes (not API routes — those handle their own auth)
        if (isAdminRoute && secret) {
            const cookie = parseCookie(request.headers.get('cookie') ?? '');
            const token = cookie['actuate_session'];
            if (!token) {
                return null;
            }
            try {
                await verifySession(token, { secret });
            }
            catch {
                return new Response(null, {
                    status: 302,
                    headers: {
                        // Expired/invalid session: send the user where they can sign in
                        // again — the dedicated login mount on split-login installs.
                        Location: loginPath ?? adminPath,
                        'Set-Cookie': 'actuate_session=; Path=/; HttpOnly; Max-Age=0',
                    },
                });
            }
        }
        return null;
    };
}
/**
 * Actionable error page shown when the admin UI is requested but `CMS_SECRET`
 * is not configured. Self-contained HTML (no external assets, no theme access)
 * because it renders before the admin shell can load. Deliberately leaks no
 * secret values — only the variable name and remediation steps.
 */
function configMissingResponse() {
    const html = `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Actuate CMS — Configuration required</title>
<style>
  :root { color-scheme: light dark; }
  body { margin: 0; min-height: 100vh; display: grid; place-items: center;
    font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
    background: #0b0d12; color: #e6e8eb; line-height: 1.5; padding: 24px; }
  .card { max-width: 560px; width: 100%; background: #151922; border: 1px solid #262c38;
    border-radius: 12px; padding: 28px 32px; }
  h1 { font-size: 1.25rem; margin: 0 0 8px; font-weight: 500; }
  p { margin: 0 0 12px; color: #aab2bd; }
  code { background: #0b0d12; border: 1px solid #262c38; border-radius: 6px;
    padding: 2px 6px; font-size: 0.9em; }
  pre { background: #0b0d12; border: 1px solid #262c38; border-radius: 8px;
    padding: 12px 14px; overflow-x: auto; font-size: 0.85rem; color: #e6e8eb; }
  ol { margin: 0 0 4px; padding-left: 20px; color: #aab2bd; }
  li { margin-bottom: 6px; }
</style>
</head>
<body>
  <main class="card">
    <h1>Actuate CMS isn't configured yet</h1>
    <p>The <code>CMS_SECRET</code> environment variable is missing or empty. It's
    required to sign admin sessions, so the admin panel can't start without it.</p>
    <ol>
      <li>Generate a secret (at least 32 characters):
        <pre>node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"</pre>
      </li>
      <li>Set <code>CMS_SECRET</code> to that value in your environment (or your
      host's environment variables), then restart / redeploy.</li>
    </ol>
    <p>This page is only visible to operators — visitors never see it.</p>
  </main>
</body>
</html>`;
    return new Response(html, {
        status: 503,
        headers: {
            'Content-Type': 'text/html; charset=utf-8',
            'Cache-Control': 'no-store',
        },
    });
}
/**
 * Match a pathname against a route prefix on a path-segment boundary, so the
 * admin guard for `/admin` does not also capture sibling public routes like
 * `/administrators` or `/admin-news`. A path matches when it equals the prefix
 * exactly or continues with a `/`.
 */
function pathMatchesPrefix(pathname, prefix) {
    const normalized = prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;
    return pathname === normalized || pathname.startsWith(`${normalized}/`);
}
function parseCookie(cookieHeader) {
    const cookies = {};
    for (const pair of cookieHeader.split(';')) {
        const [key, ...rest] = pair.split('=');
        if (key) {
            cookies[key.trim()] = rest.join('=').trim();
        }
    }
    return cookies;
}
//# sourceMappingURL=middleware.js.map