/**
* Makes an app's own HTML caching survive skew protection.
*
* Freshness is not this module's decision. How often a page changes is the
* app's knowledge, it differs per route, and Nuxt already has a way to say it:
*
* ```ts
* routeRules: {
* '/gh/**': { headers: { 'cache-control': 'public, s-maxage=300' } },
* '/blog/**': { headers: { 'cache-control': 'public, s-maxage=3600' } },
* }
* ```
*
* What this module knows is different and singular: how long a document may
* outlive the build that rendered it, which falls out of `retentionDays` and
* `maxNumberOfVersions`. So it contributes two things and invents no config of
* its own.
*
* First, it gets out of the way. `set-skew-protection-cookie` puts `__nkpv` on
* every document, and shared caches will not store a response carrying
* Set-Cookie, so the rule above silently does nothing today. The version cookie
* is dropped from exactly the documents a shared cache was asked to keep.
*
* Second, it states the bound. Any route whose declared window exceeds what
* retention can promise is named at build time, because a document that
* outlives its chunks is the failure this module exists to prevent.
*
* There is no option to turn this on. Writing the route rule is the opt-in, and
* a flag would only mean the rule keeps silently doing nothing until the author
* finds a second thing to write. What the response asks for is the whole input,
* so a route that says nothing about caching is never touched.
*/
export type HtmlCacheSkipReason = 'not-cacheable-method' | 'not-document' | 'request-has-cookie' | 'request-is-authenticated' | 'not-ok-status' | 'no-shared-cache-directive';
export type HtmlCacheDecision = {
_tag: 'shared-cacheable';
seconds: number;
} | {
_tag: 'skipped';
reason: HtmlCacheSkipReason;
};
export interface HtmlCacheRequest {
method: string;
secFetchDest: string | undefined;
accept: string | undefined;
cookie: string | undefined;
/** Any credential that personalises a document without being a cookie. */
authorization: string | undefined;
}
/**
* How long a shared cache may hold this response, in seconds.
*
* Returns null when the app did not ask for shared caching. `private` and
* `no-store` are refusals. `s-maxage` beats `max-age` because it is the one
* addressed to shared caches, which is what a stale document is served from.
*/
export declare function sharedCacheSeconds(cacheControl: unknown): number | null;
/**
* Whether a shared-cache window came only from `max-age`.
*
* `s-maxage` is read by shared caches and ignored by browsers, so writing it is
* proof the author meant a CDN. `max-age` is also how you say "browser, hold
* this", and nothing in the header separates that intent from CDN intent. The
* response is storable by a shared cache either way, so this does not change
* what the policy decides. It exists so the build can name the routes where the
* author may not have meant it.
*/
export declare function sharedWindowFromMaxAgeAlone(cacheControl: unknown): boolean;
/**
* Whether this response is one a shared cache was asked to keep.
*
* The app's `cache-control` is the whole input. However it got there, route
* rules, a nitro plugin, or a handler, the answer is the same, so an app with
* its own cache layer needs no special case.
*/
export declare function resolveHtmlCachePolicy(request: HtmlCacheRequest, response: {
status: number;
cacheControl: unknown;
}): HtmlCacheDecision;
/**
* The same Set-Cookie header with one cookie removed.
*
* Only the version cookie goes. An app cookie set during render is not ours to
* drop, and a response still carrying one simply will not be stored, which is
* the correct outcome.
*/
export declare function withoutCookie(setCookie: string | string[] | undefined, name: string): string[];
/**
* The longest a document may safely outlive the build that rendered it.
*
* This is the number to clamp a route rule against:
*
* ```ts
* const sMaxAge = Math.min(300, skewCacheCeilingSeconds(retentionDays))
* ```
*
* `retentionDays` is the only bound expressible without knowing the deploy
* rate. `maxNumberOfVersions` is the tighter one in practice and cannot be
* checked here, which is what the build-time guidance says out loud.
*/
export declare function skewCacheCeilingSeconds(retentionDays: number): number;
/**
* A route rule as far as this check is concerned.
*
* Nitro accepts several spellings of "cache this", and they do not all end up
* in `headers`. `swr` and `isr` are normalised into `cache` during nitro's own
* config pass and only become a `cache-control` header at request time, so a
* check that reads `headers` alone sees nothing and stays silent on exactly the
* rules most likely to outlive retention: `swr: 31536000` is a year.
*/
export interface InspectableRouteRule {
headers?: Record;
cache?: unknown;
swr?: boolean | number;
isr?: unknown;
}
export interface OverlongRoute {
route: string;
seconds: number;
/** Which spelling declared it, so the warning names the line to edit. */
source: 'cache-control' | 'cache' | 'swr' | 'isr';
/** The window came from `max-age` alone, so CDN intent is a guess. */
fromMaxAgeAlone: boolean;
}
declare const SHARED_CACHE_CONTROL_HEADERS: readonly ["cloudflare-cdn-cache-control", "cdn-cache-control", "cache-control"];
interface SharedCacheControlHeader {
name: typeof SHARED_CACHE_CONTROL_HEADERS[number];
value: unknown;
}
/** The highest-precedence header that states a shared-cache policy. */
export declare function sharedCacheControlHeader(getHeader: (name: string) => unknown): SharedCacheControlHeader | null;
/**
* Route rules that ask a shared cache to keep a document.
*
* Reads the app's own `routeRules` rather than a second copy of them. Public
* asset prefixes are excluded because their cached responses are not HTML.
* Empty means the app never asked for shared HTML caching in its config.
*/
export declare function cachingRouteRules(routeRules: Record, publicAssetBaseURLs?: readonly string[]): OverlongRoute[];
/** Route rules whose declared window outlives the retained builds. */
export declare function overlongRouteRules(routeRules: Record, ceilingSeconds: number, publicAssetBaseURLs?: readonly string[]): OverlongRoute[];
/**
* Reads the request fields the policy needs off an H3 event.
*
* Takes the header getter rather than importing one, so this module stays free
* of a runtime dependency and the policy is testable as plain data.
*/
export declare function htmlCacheRequestFromEvent(event: {
method?: string;
node?: {
req?: {
method?: string;
};
};
}, getHeader: (event: never, name: string) => string | undefined): HtmlCacheRequest;
/**
* The response's `Set-Cookie` entries, one per cookie, across h3 majors.
*
* This cannot go through `getResponseHeader`. On h3 v2 the response headers are
* a `Headers` instance, and `Headers.get('set-cookie')` returns every cookie
* joined with `", "` as one string. Filtering that string treats two cookies as
* one value: measured on h3 2.0.1, a `__nkpv` set before an app's `session`
* cookie produced a single joined value beginning `__nkpv=`, so a prefix filter
* dropped both and deleted the app's cookie. `getSetCookie()` is the only
* accessor that separates them, and it exists solely on `Headers`.
*
* h3 v1 appends each cookie as its own header, so the array comes back from
* `getResponseHeader` directly. Feature detection rather than a version check,
* because the shape is what matters.
*/
export declare function readSetCookies(event: {
res?: {
headers?: {
getSetCookie?: () => string[];
};
};
}, fallback: string | string[] | number | undefined): string[];
/**
* What this module can promise another module about cached documents.
*
* Duplicated verbatim in `@harlan-zw/nuxt-cloudflare`, which forces
* `private, no-store` on HTML because a cached document can name chunks a later
* deploy deleted. Retention is the answer to that, so this is how retention is
* stated in a form another module can act on.
*
* It states a bound and never an instruction. A module should not be able to
* tell another module to lower a safety rail; it can only supply the number the
* other module needs to make its own decision.
*
* Kept to a versioned, field-only interface so the two copies cannot drift in
* behaviour, only in whether they recognise a version. A consumer that reads an
* unknown `v` is expected to ignore it rather than guess.
*/
export interface HtmlCacheCapability {
v: 1;
by: string;
/** Seconds a document may outlive its build and still resolve every chunk. */
documentTtlCeilingSeconds: number;
basis: 'observed-retained-builds' | 'retention-days' | 'none';
/** Requests for a retired build's chunks resolve instead of 404. */
assetRecovery: boolean;
}
/**
* The capability this configuration supports, or null when it supports none.
*
* `assetRecovery` is the load-bearing field, not the ceiling. Retaining old
* builds is what turns a stale document from a `ChunkLoadError` into a slow
* page, and a consumer is expected to refuse the whole handshake without it.
* It is true only when this module actually stores asset bytes: the preset
* says where old builds would be served from, `bundleAssets` and `storage`
* decide whether any were kept.
*
* The ceiling is the smaller of what time allows and what rank allows, because
* whichever binds first is the one that ends the guarantee.
*/
export declare function htmlCacheCapability(input: {
retentionDays: number;
maxNumberOfVersions: number;
assetRecovery: boolean;
}): HtmlCacheCapability | null;
export {};