import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; import { createArtifactEndpointContract } from "../../dockerfiles/hermes-webui-hux/artifacts/endpoints.ts"; import { ARTIFACTS_FLAG, FOUNDATION_FLAG, PROJECTS_FLAG, artifactsEnabled, normalizeArtifact, normalizeArtifactWorkspace, normalizePreview, preservesImmutableHistory, } from "../../dockerfiles/hermes-webui-hux/artifacts/model.ts"; import { ArtifactContractError, isMime, isOpaqueId, isSha256, isUtc, normalizeIdentity, normalizeScope, safeBlobUrl, safePreviewText, safeText, sameIdentity, sameScope, scopedPath, } from "../../dockerfiles/hermes-webui-hux/artifacts/security.ts"; const IDENTITY = { tenantRef: "tnt_0123456789abcdef", userRef: "usr_0123456789abcdef", surface: "chat", }; const RAW_IDENTITY = { tenant_ref: IDENTITY.tenantRef, user_ref: IDENTITY.userRef, surface: IDENTITY.surface, }; const SCOPE = {projectId: "prj_test1234", conversationId: "conv_test1234"}; const RAW_SCOPE = {project_id: SCOPE.projectId, conversation_id: SCOPE.conversationId}; const FLAGS = [FOUNDATION_FLAG, PROJECTS_FLAG, ARTIFACTS_FLAG]; const HASH_A = `sha256:${"a".repeat(64)}`; const HASH_B = `sha256:${"b".repeat(64)}`; const CONTRACT_TYPES = JSON.parse( readFileSync("services/hermes/contracts/hux/artifact.schema.json"), ).properties.type.enum; function version(number = 1, extra = {}) { return { version: number, created_at: `2026-08-24T10:00:0${number}Z`, created_by: {type: "assistant", id: "hermes", display: "Hermes"}, content_ref: {hash: number === 1 ? HASH_A : HASH_B, bytes: 120 * number, mime: "text/markdown"}, ...(number > 1 ? {diff_from: number - 1} : {}), note: `Draft ${number}`, ...extra, }; } function artifact(extra = {}) { return { schema: "hux.artifact.v1", id: "art_test1234", owner: IDENTITY.userRef, conversation_id: SCOPE.conversationId, project_id: SCOPE.projectId, type: "markdown", language: "report", title: "Release report", current_version: 2, versions: [version(1), version(2)], sensitivity: "personal", created_at: "2026-08-24T10:00:01Z", updated_at: "2026-08-24T10:00:02Z", ...extra, }; } function page(artifacts = [artifact()], extra = {}) { return { schema: "hux.artifact_workspace.v1", api_version: "hux.v1", identity: RAW_IDENTITY, binding: RAW_SCOPE, artifacts, attachments: [{artifact_id: "art_test1234", source_ids: ["src_test1234"], citation_ids: ["cit_test1234"]}], authorizations: [{artifact_id: "art_test1234", download: "authorized", share: "requires_approval"}], ...extra, }; } test("artifacts require the foundation and projects", () => { assert.equal(artifactsEnabled(), false); assert.equal(artifactsEnabled([]), false); assert.equal(artifactsEnabled([ARTIFACTS_FLAG]), false); assert.equal(artifactsEnabled([FOUNDATION_FLAG, ARTIFACTS_FLAG]), false); assert.equal(artifactsEnabled([PROJECTS_FLAG, ARTIFACTS_FLAG]), false); assert.equal(artifactsEnabled(FLAGS), true); }); test("security primitives reject malformed scope and unsafe display data", () => { assert.equal(isOpaqueId("art_test1234", "art"), true); assert.equal(isOpaqueId("prj_test1234", "art"), false); assert.equal(isOpaqueId("bad"), false); assert.equal(isOpaqueId(4), false); assert.equal(isUtc("2026-08-24T10:00:00.123Z"), true); assert.equal(isUtc("2026-99-24T10:00:00Z"), false); assert.equal(isUtc(4), false); assert.equal(isSha256(HASH_A), true); assert.equal(isSha256(`sha256:${"z".repeat(64)}`), false); assert.equal(isSha256(4), false); assert.equal(isMime("text/plain"), true); assert.equal(isMime("bad"), false); assert.equal(isMime("a/" + "b".repeat(121)), false); assert.equal(isMime(4), false); assert.equal(safeText({}, "fallback", 20), "fallback"); assert.equal(safeText(" \n ", "fallback", 20), "fallback"); assert.equal(safeText("abc\u0000def", "fallback", 20), "abc def"); assert.equal(safeText("abcdef", "fallback", 4), "abc…"); assert.equal(safeText("token=secret Bearer abc.DEF", "fallback", 200), "[redacted] [redacted]"); assert.match(safeText("-----BEGIN PRIVATE KEY----- secret -----END PRIVATE KEY-----", "x", 200), /redacted/); assert.equal(safePreviewText("line one\nline two"), "line one\nline two"); assert.equal(safePreviewText("bad\u0000content"), null); assert.equal(safePreviewText("abc", 2), null); assert.equal(safePreviewText({}), null); assert.equal(safeBlobUrl("blob:https://chat.example/abc"), "blob:https://chat.example/abc"); for (const unsafe of ["https://example.test/image.png", "data:image/png;base64,x", "blob:has space", 4]) { assert.equal(safeBlobUrl(unsafe), null); } }); test("identity and binding normalization fail closed", () => { assert.deepEqual(normalizeIdentity(RAW_IDENTITY), IDENTITY); assert.deepEqual(normalizeScope(RAW_SCOPE), SCOPE); assert.equal(sameIdentity(IDENTITY, {...IDENTITY}), true); assert.equal(sameIdentity(IDENTITY, {...IDENTITY, surface: "worker"}), false); assert.equal(sameScope(SCOPE, {...SCOPE}), true); assert.equal(sameScope(SCOPE, {...SCOPE, projectId: "prj_other1234"}), false); for (const bad of [null, [], {}, {...RAW_IDENTITY, tenant_ref: "tenant"}, {...RAW_IDENTITY, user_ref: "user"}, {...RAW_IDENTITY, surface: "browser"}]) { assert.throws(() => normalizeIdentity(bad), ArtifactContractError); } for (const bad of [null, [], {}, {...RAW_SCOPE, project_id: "bad"}, {...RAW_SCOPE, conversation_id: "bad"}]) { assert.throws(() => normalizeScope(bad), ArtifactContractError); } }); test("schema types select safe renderers without live HTML or SVG", () => { const cases = [ ["markdown", "text/markdown", "", "document"], ["markdown", "text/plain", "research", "report"], ["document", "application/pdf", "analysis", "report"], ["document", "text/plain", "", "document"], ["code", "text/x-python", "python", "code"], ["html", "text/html", "html", "code"], ["svg", "image/svg+xml", "svg", "code"], ["json", "application/json", "json", "data"], ["json", "text/plain", "json", "data"], ["csv", "text/csv", "csv", "data"], ["csv", "text/plain", "csv", "data"], ["image", "image/png", "", "image"], ["image", "image/jpeg", "", "image"], ["image", "image/webp", "", "image"], ["image", "image/gif", "", "image"], ["audio", "audio/wav", "", "audio"], ["audio", "audio/mpeg", "", "audio"], ["audio", "audio/ogg", "", "audio"], ["audio", "audio/webm", "", "audio"], ]; assert.deepEqual(new Set(cases.map(([type]) => type)), new Set(CONTRACT_TYPES)); for (const [type, mime, language, renderer] of cases) { const raw = artifact({type, language, current_version: 1, versions: [version(1, {content_ref: {hash: HASH_A, bytes: 1, mime}})]}); assert.equal(normalizeArtifact(raw, IDENTITY.userRef, SCOPE)?.renderer, renderer); } }); test("valid artifact preserves immutable content metadata and promotion", () => { const raw = artifact({ promotion: {project_id: SCOPE.projectId, version: 1, at: "2026-08-24T10:00:03Z"}, versions: [version(1), version(2, { lineage: {artifact_id: "art_source1234", version: 3}, created_by: {type: "user", id: "private-user"}, })], }); const item = normalizeArtifact(raw, IDENTITY.userRef, SCOPE); assert.equal(item.promotedVersion, 1); assert.equal(item.renderer, "report"); assert.equal(item.versions[1].createdBy, "user"); assert.deepEqual(item.versions[1].lineage, {artifactId: "art_source1234", version: 3}); assert.doesNotMatch(JSON.stringify(item), /private-user|Hermes/); }); test("artifact records reject malformed, rewritten, and cross-scope history", () => { const bad = [ artifact({schema: "hux.artifact.v0"}), artifact({id: "bad"}), artifact({owner: "usr_ffffffffffffffff"}), artifact({project_id: "prj_other1234"}), artifact({conversation_id: "conv_other1234"}), artifact({type: "binary"}), artifact({current_version: 0}), artifact({current_version: 3}), artifact({versions: []}), artifact({versions: "no"}), artifact({versions: Array.from({length: 201}, () => version(1))}), artifact({sensitivity: "secret"}), artifact({created_at: "today"}), artifact({updated_at: "today"}), artifact({updated_at: "2026-08-24T09:00:00Z"}), artifact({title: ""}), artifact({title: {private: true}}), artifact({versions: [version(1), version(1)]}), artifact({versions: [version(1), version(2, {created_at: "2026-08-24T09:00:00Z"})]}), artifact({versions: [version(1), version(2, {diff_from: 2})]}), artifact({versions: [version(1), version(2, {lineage: {artifact_id: "art_test1234", version: 2}})]}), artifact({promotion: null}), artifact({promotion: {project_id: "prj_other1234", version: 1, at: "2026-08-24T10:00:03Z"}}), artifact({promotion: {project_id: SCOPE.projectId, version: 3, at: "2026-08-24T10:00:03Z"}}), artifact({promotion: {project_id: SCOPE.projectId, version: 1, at: "today"}}), artifact({type: "image", current_version: 1, versions: [version(1)]}), ]; for (const raw of bad) assert.equal(normalizeArtifact(raw, IDENTITY.userRef, SCOPE), null); const badVersions = [ version(1, {version: 0}), version(1, {created_at: "today"}), version(1, {created_by: null}), version(1, {created_by: {type: "intruder", id: "x"}}), version(1, {created_by: {type: "user", id: ""}}), version(1, {content_ref: null}), version(1, {content_ref: {hash: "bad", bytes: 1, mime: "text/markdown"}}), version(1, {content_ref: {hash: HASH_A, bytes: -1, mime: "text/markdown"}}), version(1, {content_ref: {hash: HASH_A, bytes: 1, mime: "bad"}}), version(1, {diff_from: 0}), version(1, {lineage: null}), version(1, {lineage: {artifact_id: "bad", version: 1}}), version(1, {lineage: {artifact_id: "art_source1234", version: 0}}), ]; for (const invalid of badVersions) { assert.equal(normalizeArtifact(artifact({current_version: 1, versions: [invalid]}), IDENTITY.userRef, SCOPE), null); } }); test("workspace envelope enforces tenant, user, project, and conversation binding", () => { const result = normalizeArtifactWorkspace(page(), IDENTITY, SCOPE); assert.equal(result.rejected, 0); assert.equal(result.artifacts.length, 1); assert.deepEqual(result.attachments, [{artifactId: "art_test1234", sourceCount: 1, citationCount: 1}]); assert.deepEqual(result.authorizations, [{artifactId: "art_test1234", download: "authorized", share: "requires_approval"}]); for (const bad of [null, {schema: "old"}, page([], {api_version: "hux.v0"}), page([], {identity: {...RAW_IDENTITY, tenant_ref: "tnt_ffffffffffffffff"}}), page([], {identity: {...RAW_IDENTITY, user_ref: "usr_ffffffffffffffff"}}), page([], {identity: {...RAW_IDENTITY, surface: "worker"}}), page([], {binding: {...RAW_SCOPE, project_id: "prj_other1234"}}), page([], {binding: {...RAW_SCOPE, conversation_id: "conv_other1234"}})]) { assert.throws(() => normalizeArtifactWorkspace(bad, IDENTITY, SCOPE), /scope boundary|identity/); } }); test("workspace rejects bad entries and ignores unsafe attachment metadata", () => { const duplicate = artifact(); const result = normalizeArtifactWorkspace(page([artifact(), duplicate, artifact({id: "bad"})], { attachments: [ {artifact_id: "art_test1234", source_ids: ["src_test1234"], citation_ids: []}, {artifact_id: "art_test1234", source_ids: [], citation_ids: []}, {artifact_id: "art_unknown1", source_ids: [], citation_ids: []}, {artifact_id: "art_other1234", source_ids: ["bad"], citation_ids: []}, null, ], authorizations: [ {artifact_id: "art_test1234", download: "authorized", share: "unavailable"}, {artifact_id: "art_test1234", download: "authorized", share: "authorized"}, {artifact_id: "art_unknown1", download: "authorized", share: "authorized"}, {artifact_id: "art_other1234", download: "yes", share: "authorized"}, null, ], }), IDENTITY, SCOPE); assert.equal(result.artifacts.length, 1); assert.equal(result.rejected, 2); assert.equal(result.attachments.length, 1); assert.equal(result.authorizations.length, 1); assert.deepEqual(normalizeArtifactWorkspace(page([], {artifacts: null, attachments: {}, authorizations: {}}), IDENTITY, SCOPE).artifacts, []); }); test("previews require matching identity, binding, hash, MIME, and local object URLs", () => { const item = normalizeArtifact(artifact(), IDENTITY.userRef, SCOPE); const base = {schema: "hux.artifact_preview.v1", identity: RAW_IDENTITY, binding: RAW_SCOPE, artifact_id: item.id, version: 2, hash: HASH_B, mime: "text/markdown", text: "# Safe report"}; assert.equal(normalizePreview(base, item, IDENTITY, SCOPE)?.text, "# Safe report"); const invalid = [null, {...base, schema: "old"}, {...base, identity: {...RAW_IDENTITY, surface: "worker"}}, {...base, binding: {...RAW_SCOPE, project_id: "prj_other1234"}}, {...base, artifact_id: "art_other1234"}, {...base, version: 3}, {...base, hash: HASH_A}, {...base, mime: "text/plain"}, {...base, text: "bad\u0000text"}, {...base, blob_url: "blob:local"}]; for (const raw of invalid) assert.equal(normalizePreview(raw, item, IDENTITY, SCOPE), null); assert.equal(normalizePreview(base, item, {...IDENTITY, userRef: "bad"}, SCOPE), null); const image = normalizeArtifact(artifact({type: "image", language: "", current_version: 1, versions: [version(1, {content_ref: {hash: HASH_A, bytes: 5, mime: "image/png"}})]}), IDENTITY.userRef, SCOPE); const imagePreview = {...base, artifact_id: image.id, version: 1, hash: HASH_A, mime: "image/png", text: undefined, blob_url: "blob:https://chat.example/image"}; assert.equal(normalizePreview(imagePreview, image, IDENTITY, SCOPE)?.blobUrl, "blob:https://chat.example/image"); assert.equal(normalizePreview({...imagePreview, blob_url: "https://remote/image"}, image, IDENTITY, SCOPE), null); assert.equal(normalizePreview({...imagePreview, text: "inline"}, image, IDENTITY, SCOPE), null); }); test("immutable history permits append-only versions but not rewrites or orphaning", () => { const before = normalizeArtifact(artifact(), IDENTITY.userRef, SCOPE); const appendedRaw = artifact({current_version: 3, updated_at: "2026-08-24T10:00:03Z", versions: [...artifact().versions, version(3, {content_ref: {hash: HASH_A, bytes: 360, mime: "text/markdown"}})]}); const appended = normalizeArtifact(appendedRaw, IDENTITY.userRef, SCOPE); assert.equal(preservesImmutableHistory(before, before), true); assert.equal(preservesImmutableHistory(before, appended), true); const changes = [ {...before, id: "art_other1234"}, {...before, owner: "usr_ffffffffffffffff"}, {...before, projectId: "prj_other1234"}, {...before, conversationId: "conv_other1234"}, {...before, type: "document"}, {...before, versions: before.versions.slice(1), currentVersion: 1}, {...before, versions: [{...before.versions[0], note: "rewritten"}, before.versions[1]]}, {...before, currentVersion: 1}, ]; for (const changed of changes) assert.equal(preservesImmutableHistory(before, changed), false); }); test("endpoint contract is default-off, scoped, immutable, and optimistic", () => { const client = {apiVersion: "hux.v1", identity: IDENTITY, endpoint(path) { assert.match(path, /^\/projects\/prj_[^/]+\/conversations\/conv_[^/]+\/artifacts/); return `/hux/v1${path}`; }}; assert.equal(createArtifactEndpointContract(client, SCOPE), null); assert.equal(createArtifactEndpointContract(client, SCOPE, [FOUNDATION_FLAG, ARTIFACTS_FLAG]), null); const contract = createArtifactEndpointContract(client, SCOPE, FLAGS); assert.equal(contract.list.path, "/hux/v1/projects/prj_test1234/conversations/conv_test1234/artifacts"); assert.equal(contract.item("art_test1234").method, "GET"); assert.match(contract.preview("art_test1234", 2).path, /versions\/2\/preview$/); assert.deepEqual(contract.diff("art_test1234", 1, 2).requiredBody, ["from_version", "to_version", "expected_current_version"]); assert.match(contract.appendVersion("art_test1234").path, /versions$/); assert.match(contract.continueEdit("art_test1234").path, /continue$/); assert.match(contract.authorize("art_test1234", "download").path, /authorizations\/download$/); assert.match(contract.download("art_test1234", 1).path, /versions\/1\/download$/); assert.match(contract.share("art_test1234", 1).path, /versions\/1\/shares$/); assert.match(contract.promote("art_test1234").path, /promotions$/); assert.match(contract.sources("art_test1234").path, /sources$/); for (const call of [() => contract.item("bad"), () => contract.preview("art_test1234", 0), () => contract.diff("art_test1234", 2, 1), () => contract.authorize("art_test1234", "execute")]) { assert.throws(call, TypeError); } assert.throws(() => createArtifactEndpointContract({...client, apiVersion: "hux.v2"}, SCOPE, FLAGS), /HUX v1/); assert.throws(() => createArtifactEndpointContract({...client, identity: {...IDENTITY, tenantRef: "bad"}}, SCOPE, FLAGS), /identity/); }); test("scoped path rejects traversal and malformed identifiers", () => { assert.equal(scopedPath("/projects", SCOPE, "art_test1234"), "/projects/prj_test1234/conversations/conv_test1234/artifacts/art_test1234"); for (const call of [() => scopedPath("https://remote", SCOPE), () => scopedPath("//remote", SCOPE), () => scopedPath("/../projects", SCOPE), () => scopedPath("/projects?x", SCOPE), () => scopedPath("/projects", {...SCOPE, projectId: "bad"}), () => scopedPath("/projects", {...SCOPE, conversationId: "bad"}), () => scopedPath("/projects", SCOPE, "bad")]) assert.throws(call, TypeError); });