From b54090e76bccbe13f1e7ae61f0fd801cd981aa0b Mon Sep 17 00:00:00 2001 From: Nathan Nguyen <146415969+NathanDrake2406@users.noreply.github.com> Date: Sat, 16 May 2026 12:02:37 +1000 Subject: [PATCH 1/2] feat(cache): classify private and dynamic render downgrades Render observations recorded dynamic request usage but did not carry an owned cacheability downgrade decision. That left future cache proof consumers to infer privacy from raw request API observations and cacheability strings. Add pure downgrade classification for public variants, private request state, auth/session dimensions, uncacheable draft state, dynamic fetches, and incomplete observations. Bump the cache proof schema for the serialized observation shape and keep the disabled proof model as the only reuse decision. --- packages/vinext/src/server/cache-proof.ts | 337 +++++++++++++++++++++- tests/cache-proof.test.ts | 158 ++++++++++ 2 files changed, 489 insertions(+), 6 deletions(-) diff --git a/packages/vinext/src/server/cache-proof.ts b/packages/vinext/src/server/cache-proof.ts index a46bfafad..0e0333f5f 100644 --- a/packages/vinext/src/server/cache-proof.ts +++ b/packages/vinext/src/server/cache-proof.ts @@ -1,8 +1,8 @@ import type { AppRouteSemanticIds } from "../routing/app-route-graph.js"; import { fnv1a64 } from "../utils/hash.js"; -export const CACHE_PROOF_MODEL_SCHEMA_VERSION = 0; -export type CacheProofModelSchemaVersion = 0; +export const CACHE_PROOF_MODEL_SCHEMA_VERSION = 1; +export type CacheProofModelSchemaVersion = 1; export type CacheProofRejectionCode = | "CP_MODEL_DISABLED" @@ -17,7 +17,8 @@ export type CacheProofRejectionCode = | "CP_ROUTE_VARIANT_CEILING_EXCEEDED" | "CP_UNSAFE_PUBLIC_DIMENSION" | "CP_BOUNDARY_OUTCOME_MISMATCH" - | "CP_BOUNDARY_OUTCOME_UNKNOWN"; + | "CP_BOUNDARY_OUTCOME_UNKNOWN" + | "CP_PRIVATE_DYNAMIC_DOWNGRADE"; export type CacheProofTraceFieldValue = string | number | boolean | null | readonly string[]; @@ -208,11 +209,93 @@ export type RenderRequestApiObservation = Readonly<{ status: RenderRequestApiStatus; }>; +export type CacheProofDowngradeTarget = + | "freshRender" + | "private" + | "privateUncacheable" + | "public" + | "publicVariant"; + +export type CacheProofDowngradeReason = + | Readonly<{ + code: "CP_DOWNGRADE_CACHEABILITY_PRIVATE"; + target: "private"; + }> + | Readonly<{ + code: "CP_DOWNGRADE_CACHEABILITY_UNCACHEABLE"; + target: "privateUncacheable"; + }> + | Readonly<{ + code: "CP_DOWNGRADE_CACHEABILITY_UNKNOWN"; + target: "freshRender"; + }> + | Readonly<{ + code: "CP_DOWNGRADE_DYNAMIC_FETCH"; + dynamicFetchCount: number; + target: "freshRender"; + }> + | Readonly<{ + code: "CP_DOWNGRADE_DYNAMIC_REQUEST_API"; + requestApi: "connection"; + target: "freshRender"; + }> + | Readonly<{ + code: "CP_DOWNGRADE_DRAFT_MODE"; + requestApi: "draftMode"; + target: "privateUncacheable"; + }> + | Readonly<{ + code: "CP_DOWNGRADE_INCOMPLETE_OBSERVATION"; + completeness: Exclude; + target: "freshRender"; + }> + | Readonly<{ + code: "CP_DOWNGRADE_PRIVATE_DIMENSION"; + inputClass: "auth" | "draft" | "private" | "session"; + source: "auth" | "cookie" | "draft-mode" | "header" | "session"; + target: "private" | "privateUncacheable"; + }> + | Readonly<{ + code: "CP_DOWNGRADE_PRIVATE_REQUEST_API"; + requestApi: "cookies" | "headers"; + target: "private"; + }> + | Readonly<{ + code: "CP_DOWNGRADE_PUBLIC_REQUEST_API"; + requestApi: "params" | "searchParams"; + target: "publicVariant"; + }> + | Readonly<{ + code: "CP_DOWNGRADE_UNKNOWN_REQUEST_API"; + requestApi: RenderRequestApiKind; + target: "freshRender"; + }>; + +export type CacheProofDowngradeClassification = Readonly<{ + canPublishPublicCache: boolean; + fallback: CacheProofBreakerFallback | null; + reasons: readonly CacheProofDowngradeReason[]; + target: CacheProofDowngradeTarget; +}>; + +export type ClassifyRenderObservationDowngradeInput = Readonly<{ + cacheability: RenderCacheability; + completeness: RenderObservationCompleteness; + dynamicFetches: readonly string[]; + requestApis: readonly RenderRequestApiObservation[]; +}>; + +export type ClassifyCacheVariantDimensionDowngradeInput = Pick< + CacheVariantDimensionInput, + "source" +>; + export type RenderObservation = Readonly<{ boundaryOutcome: BoundaryOutcome; cacheTags: readonly string[]; cacheability: RenderCacheability; completeness: RenderObservationCompleteness; + downgrade: CacheProofDowngradeClassification; dynamicFetches: readonly string[]; output: CacheProofOutputScope; pathTags: readonly string[]; @@ -592,7 +675,7 @@ export function buildCacheVariant(input: BuildCacheVariantInput): BuildCacheVari kind: "variant", variant: { schemaVersion: CACHE_PROOF_MODEL_SCHEMA_VERSION, - cacheKey: `cp0:${fnv1a64(encoded)}`, + cacheKey: `cp${CACHE_PROOF_MODEL_SCHEMA_VERSION}:${fnv1a64(encoded)}`, output: input.output, dimensions, encodedLength: encoded.length, @@ -697,6 +780,239 @@ function normalizeRequestApiObservations( .map(([kind, status]) => ({ kind, status })); } +function cacheProofDowngradeTargetRank(target: CacheProofDowngradeTarget): number { + switch (target) { + case "public": + return 0; + case "publicVariant": + return 1; + case "private": + return 2; + case "privateUncacheable": + return 3; + case "freshRender": + return 4; + default: + return assertNever(target); + } +} + +function maxCacheProofDowngradeTarget( + current: CacheProofDowngradeTarget, + candidate: CacheProofDowngradeTarget, +): CacheProofDowngradeTarget { + return cacheProofDowngradeTargetRank(candidate) > cacheProofDowngradeTargetRank(current) + ? candidate + : current; +} + +function createDowngradeFallback( + target: CacheProofDowngradeTarget, + reasons: readonly CacheProofDowngradeReason[], +): CacheProofBreakerFallback | null { + switch (target) { + case "public": + case "publicVariant": + case "private": + return null; + case "privateUncacheable": + return buildBreakerFallback( + "CP_PRIVATE_DYNAMIC_DOWNGRADE", + { + reasonCodes: reasons.map((reason) => reason.code), + target, + }, + "privateUncacheable", + ); + case "freshRender": + return buildBreakerFallback("CP_PRIVATE_DYNAMIC_DOWNGRADE", { + reasonCodes: reasons.map((reason) => reason.code), + target, + }); + default: + return assertNever(target); + } +} + +function classifyObservedRequestApiDowngrade( + kind: RenderRequestApiKind, +): CacheProofDowngradeReason { + switch (kind) { + case "connection": + return { + code: "CP_DOWNGRADE_DYNAMIC_REQUEST_API", + requestApi: "connection", + target: "freshRender", + }; + case "cookies": + return { + code: "CP_DOWNGRADE_PRIVATE_REQUEST_API", + requestApi: "cookies", + target: "private", + }; + case "draftMode": + return { + code: "CP_DOWNGRADE_DRAFT_MODE", + requestApi: "draftMode", + target: "privateUncacheable", + }; + case "headers": + return { + code: "CP_DOWNGRADE_PRIVATE_REQUEST_API", + requestApi: "headers", + target: "private", + }; + case "params": + return { + code: "CP_DOWNGRADE_PUBLIC_REQUEST_API", + requestApi: "params", + target: "publicVariant", + }; + case "searchParams": + return { + code: "CP_DOWNGRADE_PUBLIC_REQUEST_API", + requestApi: "searchParams", + target: "publicVariant", + }; + default: + return assertNever(kind); + } +} + +export function classifyCacheVariantDimensionDowngrade( + input: ClassifyCacheVariantDimensionDowngradeInput, +): CacheProofDowngradeReason | null { + switch (input.source) { + case "auth": + return { + code: "CP_DOWNGRADE_PRIVATE_DIMENSION", + inputClass: "auth", + source: "auth", + target: "private", + }; + case "cookie": + return { + code: "CP_DOWNGRADE_PRIVATE_DIMENSION", + inputClass: "private", + source: "cookie", + target: "private", + }; + case "draft-mode": + return { + code: "CP_DOWNGRADE_PRIVATE_DIMENSION", + inputClass: "draft", + source: "draft-mode", + target: "privateUncacheable", + }; + case "header": + return { + code: "CP_DOWNGRADE_PRIVATE_DIMENSION", + inputClass: "private", + source: "header", + target: "private", + }; + case "session": + return { + code: "CP_DOWNGRADE_PRIVATE_DIMENSION", + inputClass: "session", + source: "session", + target: "private", + }; + case "custom": + case "interception": + case "mounted-slots": + case "params": + case "route": + case "search": + return null; + default: + return assertNever(input.source); + } +} + +export function classifyRenderObservationDowngrade( + input: ClassifyRenderObservationDowngradeInput, +): CacheProofDowngradeClassification { + const reasons: CacheProofDowngradeReason[] = []; + let target: CacheProofDowngradeTarget = "public"; + + switch (input.cacheability) { + case "public": + break; + case "private": { + const reason = { + code: "CP_DOWNGRADE_CACHEABILITY_PRIVATE", + target: "private", + } satisfies CacheProofDowngradeReason; + reasons.push(reason); + target = maxCacheProofDowngradeTarget(target, reason.target); + break; + } + case "uncacheable": { + const reason = { + code: "CP_DOWNGRADE_CACHEABILITY_UNCACHEABLE", + target: "privateUncacheable", + } satisfies CacheProofDowngradeReason; + reasons.push(reason); + target = maxCacheProofDowngradeTarget(target, reason.target); + break; + } + case "unknown": { + const reason = { + code: "CP_DOWNGRADE_CACHEABILITY_UNKNOWN", + target: "freshRender", + } satisfies CacheProofDowngradeReason; + reasons.push(reason); + target = maxCacheProofDowngradeTarget(target, reason.target); + break; + } + default: + assertNever(input.cacheability); + } + + if (input.completeness !== "complete") { + const reason = { + code: "CP_DOWNGRADE_INCOMPLETE_OBSERVATION", + completeness: input.completeness, + target: "freshRender", + } satisfies CacheProofDowngradeReason; + reasons.push(reason); + target = maxCacheProofDowngradeTarget(target, reason.target); + } + + if (input.dynamicFetches.length > 0) { + const reason = { + code: "CP_DOWNGRADE_DYNAMIC_FETCH", + dynamicFetchCount: input.dynamicFetches.length, + target: "freshRender", + } satisfies CacheProofDowngradeReason; + reasons.push(reason); + target = maxCacheProofDowngradeTarget(target, reason.target); + } + + const requestApis = normalizeRequestApiObservations(input.requestApis); + for (const requestApi of requestApis) { + if (requestApi.status === "notObserved") continue; + const reason = + requestApi.status === "unknown" + ? ({ + code: "CP_DOWNGRADE_UNKNOWN_REQUEST_API", + requestApi: requestApi.kind, + target: "freshRender", + } satisfies CacheProofDowngradeReason) + : classifyObservedRequestApiDowngrade(requestApi.kind); + reasons.push(reason); + target = maxCacheProofDowngradeTarget(target, reason.target); + } + + return { + target, + reasons, + fallback: createDowngradeFallback(target, reasons), + canPublishPublicCache: target === "public" || target === "publicVariant", + }; +} + export function buildRenderRequestApiObservations( input: BuildRenderRequestApiObservationsInput, ): RenderRequestApiObservation[] { @@ -711,16 +1027,25 @@ export function buildRenderRequestApiObservations( } export function buildRenderObservation(input: BuildRenderObservationInput): RenderObservation { + const requestApis = normalizeRequestApiObservations(input.requestApis); + const dynamicFetches = sortedUniqueRedacted(input.dynamicFetches); + return { schemaVersion: CACHE_PROOF_MODEL_SCHEMA_VERSION, output: input.output, completeness: input.completeness, boundaryOutcome: input.boundaryOutcome, - requestApis: normalizeRequestApiObservations(input.requestApis), - dynamicFetches: sortedUniqueRedacted(input.dynamicFetches), + requestApis, + dynamicFetches, cacheTags: sortedUnique(input.cacheTags), pathTags: sortedUnique(input.pathTags), cacheability: input.cacheability, + downgrade: classifyRenderObservationDowngrade({ + cacheability: input.cacheability, + completeness: input.completeness, + dynamicFetches, + requestApis, + }), }; } diff --git a/tests/cache-proof.test.ts b/tests/cache-proof.test.ts index b6d55ba98..e70dccc8b 100644 --- a/tests/cache-proof.test.ts +++ b/tests/cache-proof.test.ts @@ -3,6 +3,9 @@ import { buildBoundaryOutcomeCompatibility, buildCacheVariant, buildRenderObservation, + CACHE_PROOF_MODEL_SCHEMA_VERSION, + classifyCacheVariantDimensionDowngrade, + classifyRenderObservationDowngrade, createAppRouteCacheProofGraphScope, createDisabledCacheProofDecision, DEFAULT_CACHE_VARIANT_BUDGET, @@ -118,6 +121,8 @@ describe("disabled cache proof model", () => { } expect(first.variant.cacheKey).toBe(second.variant.cacheKey); + expect(first.variant.cacheKey.startsWith(`cp${CACHE_PROOF_MODEL_SCHEMA_VERSION}:`)).toBe(true); + expect(first.variant.schemaVersion).toBe(CACHE_PROOF_MODEL_SCHEMA_VERSION); expect(first.variant.dimensions.map((dimension) => dimension.name)).toEqual(["id", "sort"]); expect(first.variant.dimensions[0].valueHashes).toHaveLength(1); expect(first.variant.dimensions[1].valueHashes).toHaveLength(2); @@ -370,4 +375,157 @@ describe("disabled cache proof model", () => { }, }); }); + + it("classifies public request observations as public variant dimensions", () => { + const observation = buildRenderObservation({ + boundaryOutcome: { kind: "success" }, + cacheability: "public", + cacheTags: [], + completeness: "complete", + dynamicFetches: [], + output: { + kind: "app-html", + renderEpoch: null, + rootBoundaryId: "layout:/", + routeId: "route:/products/:id", + }, + pathTags: ["/products/1"], + requestApis: [ + { kind: "params", status: "observed" }, + { kind: "searchParams", status: "observed" }, + ], + }); + + expect(observation.downgrade).toEqual(classifyRenderObservationDowngrade(observation)); + expect(observation.downgrade).toMatchObject({ + canPublishPublicCache: true, + target: "publicVariant", + fallback: null, + }); + expect(observation.downgrade.reasons.map((reason) => reason.code)).toEqual([ + "CP_DOWNGRADE_PUBLIC_REQUEST_API", + "CP_DOWNGRADE_PUBLIC_REQUEST_API", + ]); + }); + + it("classifies private auth draft and session dimensions without enabling public reuse", () => { + expect(classifyCacheVariantDimensionDowngrade({ source: "auth" })).toEqual({ + code: "CP_DOWNGRADE_PRIVATE_DIMENSION", + inputClass: "auth", + source: "auth", + target: "private", + }); + expect(classifyCacheVariantDimensionDowngrade({ source: "session" })).toEqual({ + code: "CP_DOWNGRADE_PRIVATE_DIMENSION", + inputClass: "session", + source: "session", + target: "private", + }); + expect(classifyCacheVariantDimensionDowngrade({ source: "draft-mode" })).toEqual({ + code: "CP_DOWNGRADE_PRIVATE_DIMENSION", + inputClass: "draft", + source: "draft-mode", + target: "privateUncacheable", + }); + expect(classifyCacheVariantDimensionDowngrade({ source: "cookie" })).toEqual({ + code: "CP_DOWNGRADE_PRIVATE_DIMENSION", + inputClass: "private", + source: "cookie", + target: "private", + }); + expect(classifyCacheVariantDimensionDowngrade({ source: "header" })).toEqual({ + code: "CP_DOWNGRADE_PRIVATE_DIMENSION", + inputClass: "private", + source: "header", + target: "private", + }); + expect(classifyCacheVariantDimensionDowngrade({ source: "params" })).toBeNull(); + expect(classifyCacheVariantDimensionDowngrade({ source: "search" })).toBeNull(); + }); + + it("classifies private request API observations away from public cache", () => { + const observation = buildRenderObservation({ + boundaryOutcome: { kind: "success" }, + cacheability: "public", + cacheTags: [], + completeness: "complete", + dynamicFetches: [], + output: { + kind: "app-rsc", + mountedSlotsFingerprint: null, + renderEpoch: null, + rootBoundaryId: "layout:/", + routeId: "route:/account", + }, + pathTags: ["/account"], + requestApis: [ + { kind: "cookies", status: "observed" }, + { kind: "draftMode", status: "observed" }, + { kind: "headers", status: "observed" }, + ], + }); + + expect(observation.downgrade).toMatchObject({ + canPublishPublicCache: false, + target: "privateUncacheable", + fallback: { + code: "CP_PRIVATE_DYNAMIC_DOWNGRADE", + mode: "privateUncacheable", + scope: "affectedOutput", + }, + }); + expect(observation.downgrade.reasons).toEqual([ + { + code: "CP_DOWNGRADE_PRIVATE_REQUEST_API", + requestApi: "cookies", + target: "private", + }, + { + code: "CP_DOWNGRADE_DRAFT_MODE", + requestApi: "draftMode", + target: "privateUncacheable", + }, + { + code: "CP_DOWNGRADE_PRIVATE_REQUEST_API", + requestApi: "headers", + target: "private", + }, + ]); + }); + + it("classifies dynamic and incomplete observations as fresh-render downgrades", () => { + const observation = buildRenderObservation({ + boundaryOutcome: { kind: "success" }, + cacheability: "unknown", + cacheTags: [], + completeness: "partial", + dynamicFetches: ["https://api.example.test/live?token=secret"], + output: { + kind: "app-rsc", + mountedSlotsFingerprint: null, + renderEpoch: null, + rootBoundaryId: "layout:/", + routeId: "route:/live", + }, + pathTags: ["/live"], + requestApis: [{ kind: "connection", status: "observed" }], + }); + + expect(observation.downgrade).toMatchObject({ + canPublishPublicCache: false, + target: "freshRender", + fallback: { + code: "CP_PRIVATE_DYNAMIC_DOWNGRADE", + mode: "renderFresh", + scope: "affectedOutput", + }, + }); + expect(observation.downgrade.reasons.map((reason) => reason.code)).toEqual([ + "CP_DOWNGRADE_CACHEABILITY_UNKNOWN", + "CP_DOWNGRADE_INCOMPLETE_OBSERVATION", + "CP_DOWNGRADE_DYNAMIC_FETCH", + "CP_DOWNGRADE_DYNAMIC_REQUEST_API", + ]); + expect(JSON.stringify(observation.downgrade)).not.toContain("secret"); + }); }); From 3e358d345662929bde97c5acd07fa1c8b914c93d Mon Sep 17 00:00:00 2001 From: Nathan Nguyen <146415969+NathanDrake2406@users.noreply.github.com> Date: Sat, 16 May 2026 16:40:24 +1000 Subject: [PATCH 2/2] chore(cache): rename public cache candidate flag --- packages/vinext/src/server/cache-proof.ts | 4 ++-- tests/cache-proof.test.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/vinext/src/server/cache-proof.ts b/packages/vinext/src/server/cache-proof.ts index 0e0333f5f..daeea89f6 100644 --- a/packages/vinext/src/server/cache-proof.ts +++ b/packages/vinext/src/server/cache-proof.ts @@ -272,8 +272,8 @@ export type CacheProofDowngradeReason = }>; export type CacheProofDowngradeClassification = Readonly<{ - canPublishPublicCache: boolean; fallback: CacheProofBreakerFallback | null; + isPublicCacheCandidate: boolean; reasons: readonly CacheProofDowngradeReason[]; target: CacheProofDowngradeTarget; }>; @@ -1009,7 +1009,7 @@ export function classifyRenderObservationDowngrade( target, reasons, fallback: createDowngradeFallback(target, reasons), - canPublishPublicCache: target === "public" || target === "publicVariant", + isPublicCacheCandidate: target === "public" || target === "publicVariant", }; } diff --git a/tests/cache-proof.test.ts b/tests/cache-proof.test.ts index e70dccc8b..701e0ecdd 100644 --- a/tests/cache-proof.test.ts +++ b/tests/cache-proof.test.ts @@ -398,7 +398,7 @@ describe("disabled cache proof model", () => { expect(observation.downgrade).toEqual(classifyRenderObservationDowngrade(observation)); expect(observation.downgrade).toMatchObject({ - canPublishPublicCache: true, + isPublicCacheCandidate: true, target: "publicVariant", fallback: null, }); @@ -466,7 +466,7 @@ describe("disabled cache proof model", () => { }); expect(observation.downgrade).toMatchObject({ - canPublishPublicCache: false, + isPublicCacheCandidate: false, target: "privateUncacheable", fallback: { code: "CP_PRIVATE_DYNAMIC_DOWNGRADE", @@ -512,7 +512,7 @@ describe("disabled cache proof model", () => { }); expect(observation.downgrade).toMatchObject({ - canPublishPublicCache: false, + isPublicCacheCandidate: false, target: "freshRender", fallback: { code: "CP_PRIVATE_DYNAMIC_DOWNGRADE",