import { createHash } from "node:crypto"; import { describe, expect, it } from "vitest"; import { createAssetReleaseManifest } from "../../packages/asset-release-manifest/src/index.js"; const releaseVersion = "asset-20260803.1"; const publicId = "7f0c9530-a7d9-4bf1-8c65-0e9298dd04ac"; const previewId = "ab18fd72-60e1-44e3-a9a0-3dfccb12e17c"; const privateId = "e3792605-5252-4d3b-a101-827408ab3515"; function fixture() { return createAssetReleaseManifest({ items: [ { access_class: "public_release_asset", cache_kind: "thumbnail", content: Buffer.from("public-content"), mime_type: "image/png", relative_path: "public/thumbnails/FLOWER001.png", resource_id: publicId, root_ref: "canonical-assets", }, { access_class: "internal_preview_asset", content: Buffer.from("preview-content"), mime_type: "image/webp", relative_path: "preview/batch-7/FLOWER009.webp", resource_id: previewId, root_ref: "canonical-assets", }, { access_class: "private_user_asset", content: Buffer.from("private-content"), mime_type: "image/png", owner_id: "owner-1", relative_path: "private/owner-1/generated.png", resource_id: privateId, root_ref: "managed-assets", }, ], release_version: releaseVersion, }); } describe("TASK-WP5-04 immutable asset release manifest", () => { it("projects only the selected access class and never exposes source paths", () => { const manifest = fixture(); const projected = manifest.project("public_release_asset", releaseVersion); expect(Object.isFrozen(projected)).toBe(true); expect(projected?.manifest_sha256).toMatch(/^[0-9a-f]{64}$/); expect(projected?.items).toEqual([expect.objectContaining({ access_class: "public_release_asset", resource_id: publicId, sha256: createHash("sha256").update("public-content").digest("hex"), url: `/api/v1/assets/public/${releaseVersion}/${publicId}`, })]); const serialized = JSON.stringify(projected); expect(serialized).not.toContain(previewId); expect(serialized).not.toContain(privateId); expect(serialized).not.toContain("relative_path"); expect(serialized).not.toContain("root_ref"); expect(serialized).not.toMatch(/[A-Z]:\\\\/i); }); it("keeps resource identifiers opaque, verifies file hashes, and rejects unsafe manifests", () => { const manifest = fixture(); expect(manifest.read("public_release_asset", releaseVersion, publicId)?.bytes).toEqual(Buffer.from("public-content")); expect(manifest.read("public_release_asset", releaseVersion, previewId)).toBeUndefined(); expect(manifest.project("private_user_asset", releaseVersion, { ownerId: "owner-1" })?.items) .toEqual([expect.objectContaining({ resource_id: privateId })]); expect(manifest.project("private_user_asset", releaseVersion, { ownerId: "owner-2" })?.items).toEqual([]); expect(() => createAssetReleaseManifest({ items: [{ access_class: "public_release_asset", cache_kind: "thumbnail", content: Buffer.from("tampered"), mime_type: "image/png", relative_path: "public/tampered.png", resource_id: publicId, root_ref: "canonical-assets", sha256: "0".repeat(64), }], release_version: releaseVersion, })).toThrow(/sha-?256/i); expect(() => createAssetReleaseManifest({ items: [{ access_class: "public_release_asset", cache_kind: "thumbnail", content: Buffer.from("unsafe"), mime_type: "image/png", relative_path: "/outside/private.png", resource_id: publicId, root_ref: "canonical-assets", }], release_version: releaseVersion, })).toThrow(/relative path/i); }); });