/**
 * Approximate cap on a single runtime cache entry. Vercel's hard limit
 * is 2 MB; we stop short so a slightly-larger-than-expected payload
 * doesn't surface the SDK error path. Anything bigger than this falls
 * through to a normal DB read on every request — slower, but correct.
 */
const MAX_RUNTIME_CACHE_BYTES = 1_500_000;
/** The wholesale tag added to every SDK request — panic-button invalidation. */
export const ACTUATE_TAG_ROOT = 'actuate';
/**
 * Build a single canonical tag (e.g. `actuate:collection:posts`). Mirrors
 * the SDK's `actuateTag()` so the server can construct invalidation keys
 * that match what the client will actually invalidate.
 */
export function actuateTag(kind, slug) {
    return `${ACTUATE_TAG_ROOT}:${kind}:${slug}`;
}
/**
 * Tag set to invalidate after a write. Always includes the wholesale
 * `actuate` tag plus the narrowest specific tags so consumers who only
 * tagged broadly (or only tagged narrowly) both pick up the change.
 *
 * - `create` / `update` / `delete` on a doc → collection + doc tags.
 * - `update` on a global → global tag.
 *
 * Paths (`actuate:path:<path>`) are intentionally NOT auto-invalidated
 * here: the server cannot know which paths resolve to which doc without
 * re-running the resolver, and a stale slug change is a rare-enough
 * event that the host app should handle it in its own webhook.
 */
export function invalidationTagsForDocumentWrite(collection, documentId) {
    return [ACTUATE_TAG_ROOT, actuateTag('collection', collection), actuateTag('doc', documentId)];
}
export function invalidationTagsForGlobalWrite(slug) {
    return [ACTUATE_TAG_ROOT, actuateTag('global', slug)];
}
/**
 * Build the `Cache-Control` header value for a public read response.
 *
 * Rules:
 *
 * - Session-cookie authentication ALWAYS returns `private, no-store`.
 *   Shared CDNs MUST NOT cache per-user responses, full stop.
 * - When `cache.publicReads.enabled` is false (the default), returns
 *   null — caller emits no `Cache-Control` header.
 * - When enabled and the request is unauthenticated or uses an API
 *   key, returns
 *   `public, s-maxage=<maxAge>, stale-while-revalidate=<swr>`.
 *
 * @param request The inbound request — used to detect session cookies.
 * @param cache   `ActuateCMSConfig.cache` (may be undefined).
 * @returns       The `Cache-Control` header value, or null when none should be sent.
 */
export function publicReadCacheControl(request, cache) {
    // Session-cookie auth always opts out of shared caching. This is
    // non-negotiable: emitting `public` on a session-authed response
    // could leak admin views to other users on the same CDN tier.
    if (hasSessionCookie(request)) {
        return 'private, no-store';
    }
    if (!cache?.publicReads?.enabled)
        return null;
    const maxAge = cache.publicReads.maxAge ?? 60;
    const swr = cache.publicReads.staleWhileRevalidate ?? 300;
    // Clamp negative values to 0 so a misconfigured value doesn't produce
    // a header the CDN treats as undefined behaviour.
    const safeMaxAge = Math.max(0, Math.floor(maxAge));
    const safeSwr = Math.max(0, Math.floor(swr));
    return `public, s-maxage=${safeMaxAge}, stale-while-revalidate=${safeSwr}`;
}
/**
 * Returns true when the request carries the Actuate session cookie. We
 * deliberately check the cookie NAME, not its validity — an expired or
 * tampered cookie is still associated with a single user agent, so
 * treating it as "potentially-authenticated" and refusing shared-cache
 * is the safe default.
 *
 * Exported so the runtime-cache layer can reuse the exact same bypass
 * rule as the CDN-cache layer.
 */
export function hasSessionCookie(request) {
    const cookieHeader = request.headers.get('cookie');
    if (!cookieHeader)
        return false;
    // Match `actuate_session=` as a discrete cookie name, not anywhere in
    // the header text — defense-in-depth against a cookie whose VALUE
    // happens to contain the substring.
    return /(?:^|;\s*)actuate_session=/.test(cookieHeader);
}
// ─── Runtime cache (layer 3) ────────────────────────────────────────────
/**
 * Build a stable, content-addressed cache key for a public read.
 *
 * - Path-only when the URL has no query (`/api/cms/collections/posts`).
 * - Path + sorted query when present, so `?page=2&pageSize=20` keys the
 *   same way regardless of param order.
 * - Hashing isn't done here — the runtime cache adapter is free to apply
 *   its own `keyHashFunction` if it cares about key length.
 *
 * Exported for tests + custom routes that want to opt into the same
 * cache layer.
 */
export function cacheKeyFor(request) {
    const url = new URL(request.url);
    const params = Array.from(url.searchParams.entries()).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
    if (params.length === 0)
        return url.pathname;
    const sorted = new URLSearchParams(params).toString();
    return `${url.pathname}?${sorted}`;
}
/**
 * Returns true when the caller passed `Cache-Control: no-cache` /
 * `no-store` on the request. Lets debugging tools force a cache miss
 * without flushing the entire layer.
 */
function requestOptedOut(request) {
    const cc = request.headers.get('cache-control');
    if (!cc)
        return false;
    return /(?:^|,\s*)(no-cache|no-store)\b/i.test(cc);
}
/**
 * Run a read endpoint through the optional Runtime Cache layer.
 *
 * Contract:
 *   - When `cache.runtimeCache.adapter` is unset, OR the request carries
 *     a session cookie, OR the caller sent `Cache-Control: no-cache`,
 *     this helper just invokes `build()` and returns whatever it
 *     produces (either a `Response` for early-exit cases like 404, or
 *     plain JSON-able data that gets wrapped via `wrap()`).
 *   - On cache hit, the cached payload is returned via `wrap(hit, request)`
 *     so the same CDN-tier `Cache-Control` rules apply. Hits get
 *     `X-Actuate-Cache: HIT`; misses get `X-Actuate-Cache: MISS`.
 *   - On cache miss, `build()` runs. If it returns a `Response` (e.g.
 *     404), that response is passed through untouched — we never cache
 *     error / not-found shapes. Otherwise the payload is stored under
 *     the cache key with the supplied tags and TTL.
 *   - Adapter errors (`get` or `set` throwing) are logged and swallowed.
 *     The user-visible request never fails because the cache fell over.
 *   - Payloads larger than ~1.5 MB are not stored — keeps us comfortably
 *     under the 2 MB Vercel runtime-cache item ceiling.
 */
export async function cachedRead(request, opts) {
    const adapter = opts.cache?.runtimeCache?.adapter;
    const ttl = opts.cache?.runtimeCache?.ttl ?? 60;
    // Bypass paths — no adapter, per-user request, or explicit opt-out.
    if (!adapter || hasSessionCookie(request) || requestOptedOut(request)) {
        const r = await opts.build();
        return r instanceof Response ? r : opts.wrap(r, request);
    }
    const key = cacheKeyFor(request);
    let hit;
    try {
        hit = await adapter.get(key);
    }
    catch (err) {
        // Read failures never break the request — fall through to a fresh fetch.
        console.error('[actuate][cache] runtime cache get failed', err);
        const r = await opts.build();
        return r instanceof Response ? r : opts.wrap(r, request);
    }
    if (hit !== undefined && hit !== null) {
        const res = opts.wrap(hit, request);
        res.headers.set('X-Actuate-Cache', 'HIT');
        return res;
    }
    const r = await opts.build();
    if (r instanceof Response)
        return r; // 404 / error — never cache.
    try {
        // Approximate size by serialised length; the runtime cache adapter
        // will JSON-stringify again on its way out, so the work isn't
        // duplicated wastefully — and a known-too-big payload skips the
        // write call entirely.
        const size = JSON.stringify(r)?.length ?? 0;
        if (size > 0 && size <= MAX_RUNTIME_CACHE_BYTES) {
            await adapter.set(key, r, { ttl, tags: opts.tags });
        }
    }
    catch (err) {
        // Write failures never break the request either — log and move on.
        console.error('[actuate][cache] runtime cache set failed', err);
    }
    const res = opts.wrap(r, request);
    res.headers.set('X-Actuate-Cache', 'MISS');
    return res;
}
//# sourceMappingURL=http.js.map