Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions docs/api-reference/veryfront/release-assets.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,44 +66,44 @@ const url = releaseAssetUrl("a".repeat(64), "js");

| Name | Description | Source |
| -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `clearCachedReleaseAssetManifests` | Clear cached manifest bodies while keeping registered fetchers intact. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/release-assets/manifest-cache.ts#L567) |
| `clearReleaseAssetManifestCache` | Clear the cache and fetcher registry (tests / adapter teardown). | [source](https://github.com/veryfront/veryfront-code/blob/main/src/release-assets/manifest-cache.ts#L577) |
| `clearCachedReleaseAssetManifests` | Clear cached manifest bodies while keeping registered fetchers intact. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/release-assets/manifest-cache.ts#L587) |
| `clearReleaseAssetManifestCache` | Clear the cache and fetcher registry (tests / adapter teardown). | [source](https://github.com/veryfront/veryfront-code/blob/main/src/release-assets/manifest-cache.ts#L597) |
| `contentTypeForExtension` | Resolve the content type for an extension, or null if not allowed. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/release-assets/constants.ts#L96) |
| `describeReadyReleaseAssetManifestRejection` | Explain why a ready manifest response was rejected. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/release-assets/manifest-schema.ts#L381) |
| `getReadyManifestForRender` | Return a ready manifest for `releaseId` if one is cached, else null. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/release-assets/manifest-cache.ts#L257) |
| `getReadyManifestForRenderAsync` | Await a ready manifest for rendering when release-manifest consumption is enabled. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/release-assets/manifest-cache.ts#L358) |
| `getReadyManifestForRender` | Return a ready manifest for `releaseId` if one is cached, else null. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/release-assets/manifest-cache.ts#L258) |
| `getReadyManifestForRenderAsync` | Await a ready manifest for rendering when release-manifest consumption is enabled. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/release-assets/manifest-cache.ts#L359) |
| `hasImmutableReleaseAssetDependencies` | True only when manifest dependency entries are safe immutable rewrite targets. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/release-assets/manifest-schema.ts#L283) |
| `isAllowedReleaseAssetContentType` | True when the value is a valid allowlisted release asset content type. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/release-assets/constants.ts#L105) |
| `isReleaseAssetManifestEnabled` | True when production manifest consumption is enabled via env flag. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/release-assets/manifest-cache.ts#L202) |
| `isReleaseAssetManifestEnabled` | True when production manifest consumption is enabled via env flag. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/release-assets/manifest-cache.ts#L203) |
| `isSafeBoundedText` | Check that an untrusted value is a non-empty, trimmed string within `maxLength` that contains no control characters. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/release-assets/manifest-schema.ts#L80) |
| `isValidContentHash` | Validate a content hash is exactly 64 lowercase hex characters. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/release-assets/constants.ts#L114) |
| `normalizeManifestModuleKey` | Normalize a logical module path to the manifest's key convention. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/release-assets/html-consumption.ts#L27) |
| `parseReadyReleaseAssetManifestResponse` | Parse an untrusted ready response without executing accessors. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/release-assets/manifest-schema.ts#L337) |
| `parseReleaseAssetManifest` | Parse an untrusted manifest without requiring a registered schema extension. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/release-assets/manifest-schema.ts#L322) |
| `readUntrustedOwnDataProperty` | Read an own data property from an untrusted value without invoking accessors. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/release-assets/manifest-schema.ts#L425) |
| `registerManifestFetcherForRelease` | Register a project-scoped manifest fetcher for the given releaseId. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/release-assets/manifest-cache.ts#L158) |
| `registerManifestFetcherForRelease` | Register a project-scoped manifest fetcher for the given releaseId. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/release-assets/manifest-cache.ts#L159) |
| `releaseAssetUrl` | Map a 64-hex content hash + extension to its public asset URL. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/release-assets/constants.ts#L85) |
| `resolveManifestModuleUrl` | Resolve a module URL through the manifest. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/release-assets/html-consumption.ts#L42) |
| `resolveManifestRoutePreloadUrls` | Resolve the route closure module URLs for preload hints from the manifest. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/release-assets/html-consumption.ts#L64) |
| `routeForPage` | Derive a route path from a page module logical path. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/release-assets/route-path.ts#L50) |
| `unregisterManifestFetcherForRelease` | Remove the manifest fetcher for the given releaseId. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/release-assets/manifest-cache.ts#L196) |
| `unregisterManifestFetcherForRelease` | Remove the manifest fetcher for the given releaseId. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/release-assets/manifest-cache.ts#L197) |

### Types

| Name | Description | Source |
| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `ImmutableReleaseAssetManifest` | Manifest whose dependency entries name uploaded content-addressed assets. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/release-assets/manifest-schema.ts#L278) |
| `ReadyManifestReadOptions` | Controls revalidation behavior for awaited manifest reads. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/release-assets/manifest-cache.ts#L76) |
| `ReadyManifestReadOptions` | Controls revalidation behavior for awaited manifest reads. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/release-assets/manifest-cache.ts#L77) |
| `ReadyReleaseAssetManifestResponse` | Strict ready response with a generation-matched validated manifest body. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/release-assets/manifest-schema.ts#L306) |
| `ReleaseAssetContentType` | MIME types accepted for immutable release asset uploads and responses. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/release-assets/constants.ts#L24) |
| `ReleaseAssetCssEntry` | Content-addressed CSS entry. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/release-assets/manifest-schema.ts#L272) |
| `ReleaseAssetDependencyMode` | Capability represented by entries in the manifest dependency map. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/release-assets/manifest-schema.ts#L276) |
| `ReleaseAssetEntry` | Content-addressed JavaScript module entry. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/release-assets/manifest-schema.ts#L270) |
| `ReleaseAssetExtension` | File extensions supported by the immutable release asset endpoint. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/release-assets/constants.ts#L22) |
| `ReleaseAssetManifest` | Validated, immutable release asset manifest v2 body. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/release-assets/manifest-schema.ts#L266) |
| `ReleaseAssetManifestFetchContext` | Cancellation context passed to a release-scoped manifest fetcher. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/release-assets/manifest-cache.ts#L91) |
| `ReleaseAssetManifestFetcher` | Fetcher used to retrieve a manifest for a release. Registered per-releaseId by the runtime adapter that owns that release, so the correct project-scoped token is always used. Returns null when the manifest is unavailable. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/release-assets/manifest-cache.ts#L108) |
| `ReleaseAssetManifestFetcherCleanup` | Idempotent cleanup for one fetcher registration. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/release-assets/manifest-cache.ts#L116) |
| `ReleaseAssetManifestFetchContext` | Cancellation context passed to a release-scoped manifest fetcher. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/release-assets/manifest-cache.ts#L92) |
| `ReleaseAssetManifestFetcher` | Fetcher used to retrieve a manifest for a release. Registered per-releaseId by the runtime adapter that owns that release, so the correct project-scoped token is always used. Returns null when the manifest is unavailable. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/release-assets/manifest-cache.ts#L109) |
| `ReleaseAssetManifestFetcherCleanup` | Idempotent cleanup for one fetcher registration. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/release-assets/manifest-cache.ts#L117) |
| `ReleaseAssetManifestResponse` | Response shape for the GET asset-manifest endpoint. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/release-assets/manifest-schema.ts#L299) |
| `ReleaseAssetManifestState` | Manifest lifecycle states (DB-owned; mirrored here for runtime checks). | [source](https://github.com/veryfront/veryfront-code/blob/main/src/release-assets/manifest-schema.ts#L290) |
| `ReleaseAssetRouteEntry` | Per-route module and CSS closure. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/release-assets/manifest-schema.ts#L274) |
Expand Down
67 changes: 66 additions & 1 deletion src/release-assets/manifest-cache.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import "#veryfront/schemas/_test-setup.ts";
import { assertEquals } from "#veryfront/testing/assert.ts";
import { assertEquals, assertStringIncludes } from "#veryfront/testing/assert.ts";
import { afterEach, describe, it } from "#veryfront/testing/bdd.ts";
import {
clearCachedReleaseAssetManifests,
Expand Down Expand Up @@ -76,6 +76,71 @@ describe("release asset manifest fetcher ownership", () => {
assertEquals(calls, ["newer"]);
});

it("reports why a ready manifest was rejected instead of only refusing it", async () => {
// The failure this pins: assets built by a different framework version are
// published as `ready` and refused here, which takes a site's whole client
// bundle offline. Before this, the only operator-visible signal was a 503
// and a timing mark, so version skew and a corrupt payload looked alike.
Deno.env.set("VERYFRONT_RELEASE_ASSET_MANIFEST", "1");
const skewed = manifest("release-skew", 1);
registerManifestFetcherForRelease("release-skew", () =>
Promise.resolve({
state: "ready",
manifest_version: 1,
manifest: {
...skewed,
schemaVersion: RELEASE_ASSET_MANIFEST_SCHEMA_VERSION + 1,
},
}));

const originalError = console.error;
let logged = "";
console.error = (...values: unknown[]) => {
logged += values.map((value) => typeof value === "string" ? value : JSON.stringify(value))
.join(" ");
};

try {
assertEquals(await getReadyManifestForRenderAsync("release-skew"), null);
} finally {
console.error = originalError;
}

assertStringIncludes(logged, "release-skew");
assertStringIncludes(logged, "manifest schema version");
assertStringIncludes(logged, "built by a different framework version");
});

it("reports a ready response whose envelope has no usable manifest_version", async () => {
// `state` is normalized to "invalid" when the envelope is unusable, so
// classifying on it routed this rejection to debug, silencing one of the
// exact reasons the diagnostic exists to surface. Classification follows
// the publisher's claimed state instead.
Deno.env.set("VERYFRONT_RELEASE_ASSET_MANIFEST", "1");
registerManifestFetcherForRelease("release-envelope", () =>
Promise.resolve({
state: "ready",
manifest_version: -1,
manifest: manifest("release-envelope", 1),
} as unknown as ReturnType<typeof readyManifestResponse>));

const originalError = console.error;
let logged = "";
console.error = (...values: unknown[]) => {
logged += values.map((value) => typeof value === "string" ? value : JSON.stringify(value))
.join(" ");
};

try {
assertEquals(await getReadyManifestForRenderAsync("release-envelope"), null);
} finally {
console.error = originalError;
}

assertStringIncludes(logged, "release-envelope");
assertStringIncludes(logged, "no usable manifest_version");
});

it("keeps cache identities distinct for delimiter-shaped release IDs", async () => {
Deno.env.set("VERYFRONT_RELEASE_ASSET_MANIFEST", "1");
const calls: string[] = [];
Expand Down
Loading