diff --git a/src/manifest.ts b/src/manifest.ts index 234fa59..9d0cbfe 100644 --- a/src/manifest.ts +++ b/src/manifest.ts @@ -1,6 +1,5 @@ import { z } from "zod"; - -const dockerManifestListContentType = "application/vnd.docker.distribution.manifest.list.v2+json"; +import { dockerManifestListContentType } from "./media-types"; const platformSchema = z.object({ "architecture": z.string(), diff --git a/src/media-types.ts b/src/media-types.ts new file mode 100644 index 0000000..c1a87c5 --- /dev/null +++ b/src/media-types.ts @@ -0,0 +1,10 @@ +// Named constants for the manifest media types the registry recognizes during content +// negotiation. These strings are the OCI/Docker content-negotiation contract and must stay +// byte-identical to the specification, so each is defined exactly once here and referenced +// everywhere else. This module has no imports, keeping it a safe leaf to import from anywhere. + +export const dockerManifestListContentType = "application/vnd.docker.distribution.manifest.list.v2+json"; +export const ociImageIndexContentType = "application/vnd.oci.image.index.v1+json"; +export const dockerManifestV1PrettyJwsContentType = "application/vnd.docker.distribution.manifest.v1+prettyjws"; +export const ociImageManifestContentType = "application/vnd.oci.image.manifest.v1+json"; +export const dockerManifestV2ContentType = "application/vnd.docker.distribution.manifest.v2+json"; diff --git a/src/registry/http.ts b/src/registry/http.ts index 2b3dda8..309967a 100644 --- a/src/registry/http.ts +++ b/src/registry/http.ts @@ -18,7 +18,13 @@ import { UploadId, UploadObject, } from "./registry"; -import { ociImageIndexContentType } from "./r2"; +import { + dockerManifestListContentType, + ociImageIndexContentType, + dockerManifestV1PrettyJwsContentType, + ociImageManifestContentType, + dockerManifestV2ContentType, +} from "../media-types"; type AuthContext = { authType: AuthType; @@ -45,12 +51,12 @@ type HTTPContext = { }; export const manifestTypes = [ - "application/vnd.docker.distribution.manifest.list.v2+json", - "application/vnd.oci.image.index.v1+json", - "application/vnd.docker.distribution.manifest.v1+prettyjws", + dockerManifestListContentType, + ociImageIndexContentType, + dockerManifestV1PrettyJwsContentType, "application/json", - "application/vnd.oci.image.manifest.v1+json", - "application/vnd.docker.distribution.manifest.v2+json", + ociImageManifestContentType, + dockerManifestV2ContentType, ] as const; export type ManifestType = (typeof manifestTypes)[number]; diff --git a/src/registry/r2.ts b/src/registry/r2.ts index 7f01098..8fa4cbd 100644 --- a/src/registry/r2.ts +++ b/src/registry/r2.ts @@ -32,8 +32,6 @@ import { import { GarbageCollectionMode, GarbageCollector } from "./garbage-collector"; import { ManifestSchema, manifestSchema } from "../manifest"; -export const ociImageIndexContentType = "application/vnd.oci.image.index.v1+json"; - function referrersPrefix(name: string, digest: string): string { return `${name}/_referrers/${digest}/`; } diff --git a/src/router.ts b/src/router.ts index f8de37a..14055d7 100644 --- a/src/router.ts +++ b/src/router.ts @@ -18,7 +18,7 @@ import { registries, } from "./registry/registry"; import { RegistryHTTPClient } from "./registry/http"; -import { ociImageIndexContentType } from "./registry/r2"; +import { ociImageIndexContentType } from "./media-types"; const maxReferrersListLimit = 1000; const isOpaqueReferrersCursor = (cursor: string) => cursor.startsWith("/v2/"); diff --git a/test/media-types.test.ts b/test/media-types.test.ts new file mode 100644 index 0000000..85357b4 --- /dev/null +++ b/test/media-types.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, test } from "vitest"; +import { manifestTypes } from "../src/registry/http"; +import { + dockerManifestListContentType, + ociImageIndexContentType, + dockerManifestV1PrettyJwsContentType, + ociImageManifestContentType, + dockerManifestV2ContentType, +} from "../src/media-types"; + +describe("manifest media types", () => { + // The Accept header is emitted as manifestTypes.join(", "), so both the exact string values + // and the array order are part of the content-negotiation wire contract. This pins them so a + // value edit or a reorder fails here rather than silently changing what the registry sends. + test("manifestTypes holds the expected set in the expected order", () => { + expect(manifestTypes).toEqual([ + "application/vnd.docker.distribution.manifest.list.v2+json", + "application/vnd.oci.image.index.v1+json", + "application/vnd.docker.distribution.manifest.v1+prettyjws", + "application/json", + "application/vnd.oci.image.manifest.v1+json", + "application/vnd.docker.distribution.manifest.v2+json", + ]); + }); + + test("each manifest media-type constant holds its spec string", () => { + expect(dockerManifestListContentType).toBe("application/vnd.docker.distribution.manifest.list.v2+json"); + expect(ociImageIndexContentType).toBe("application/vnd.oci.image.index.v1+json"); + expect(dockerManifestV1PrettyJwsContentType).toBe("application/vnd.docker.distribution.manifest.v1+prettyjws"); + expect(ociImageManifestContentType).toBe("application/vnd.oci.image.manifest.v1+json"); + expect(dockerManifestV2ContentType).toBe("application/vnd.docker.distribution.manifest.v2+json"); + }); +});