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
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,19 @@ The package remains private at version `0.1.0` to prevent accidental
publication before the Host-stamped service configuration revision and final
consumer removal are complete.

## Plugin brand artwork

The selected 256×256 PNG is owned at `assets/channel.png` and included in the
package file allowlist. `src/brand-icon.ts` embeds the same bytes in the public
module `icon` export (`CordisXPluginBrandIcon` from `cordisx/contracts`), which
Host validates and renders in its plugin list. This does not replace semantic
menu or action icons. Run `node scripts/generate-brand-icon.mjs` after an
approved artwork replacement; the package tests verify the bundled icon bytes.

Artwork ships through this repository's source distribution on `main`.
The package remains private at `0.1.0`; this change does not publish an npm
release or introduce a separate artifact release channel.

## Notification feedback

See [operation notifications and development dependencies](./.agents/docs/notifications.md).
Binary file added assets/channel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"types": "./dist/channel.d.ts",
"files": [
"dist",
"assets/channel.png",
"cordisx-package.json",
"runtime-manifest.json",
"README.md",
Expand All @@ -32,8 +33,9 @@
"lint": "npm run lint:source && npm run lint:styles",
"format": "dprint fmt",
"format:check": "dprint check",
"check": "node --check src/service.mjs && npm run format:check && npm run lint && npm run typecheck && npm test",
"prepare": "npm run build"
"check": "node --check src/service.mjs && npm run format:check && npm run lint && npm run typecheck && npm test && npm run check:package",
"prepare": "npm run build",
"check:package": "node scripts/check-package.mjs"
},
"engines": {
"node": ">=22.19"
Expand Down
20 changes: 20 additions & 0 deletions scripts/check-package.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { mkdtemp, readdir, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";

const destination = await mkdtemp(join(tmpdir(), "channel-package-"));
try {
execFileSync("npm", ["pack", "--pack-destination", destination], { stdio: "inherit" });
const archives = (await readdir(destination)).filter(name => name.endsWith(".tgz"));
assert.equal(archives.length, 1);
const archive = join(destination, archives[0]);
const image = execFileSync("tar", ["-xOf", archive, "package/assets/channel.png"]);
assert.deepEqual(image, await readFile(new URL("../assets/channel.png", import.meta.url)));
const module = execFileSync("tar", ["-xOf", archive, "package/dist/channel.js"], { encoding: "utf8" });
assert.equal(module, await readFile(new URL("../dist/channel.js", import.meta.url), "utf8"));
console.log("Package contains the exact selected PNG and validated runtime module.");
} finally {
await rm(destination, { recursive: true, force: true });
}
12 changes: 12 additions & 0 deletions scripts/generate-brand-icon.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { readFile, writeFile } from "node:fs/promises";

const image = await readFile(new URL("../assets/channel.png", import.meta.url));
const lines = image.toString("base64").match(/.{1,100}/g);
await writeFile(
new URL("../src/brand-icon.ts", import.meta.url),
"// Generated from assets/channel.png; regenerate with node scripts/generate-brand-icon.mjs.\n"
+ "import type { CordisXPluginBrandIcon } from \"cordisx/contracts\";\n\n"
+ "export const icon = {\n mediaType: \"image/png\",\n data: [\n"
+ lines.map(line => ` "${line}",`).join("\n")
+ "\n ].join(\"\"),\n} satisfies CordisXPluginBrandIcon;\n",
);
786 changes: 786 additions & 0 deletions src/brand-icon.ts

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
import { defineReactPage } from "cordisx/react";
import { createChannelPage } from "./page.js";

export { icon } from "./brand-icon.js";

export const name = "channel";
export const inject = ["i18n", "slots", "pages", "routes", "managerContent", "channelManager", "notifications"];

Expand Down
14 changes: 14 additions & 0 deletions test/package-artifact.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,17 @@ test("keeps source, runtime manifest, and bundled module identity aligned", asyn
await readFile(new URL(`..${packageManifest.entry.slice(1)}`, import.meta.url));
await readFile(new URL(`..${declared.services[0].entry.slice(1)}`, import.meta.url));
});

test("ships the selected brand PNG unchanged through the public module icon", async () => {
const image = await readFile(new URL("../assets/channel.png", import.meta.url));
const { icon } = await import("../dist/channel.js");
assert.equal(icon.mediaType, "image/png");
assert.ok(icon.data.length <= 400_000);
assert.deepEqual(Buffer.from(icon.data, "base64"), image);
assert.equal(image.readUInt32BE(16), 256);
assert.equal(image.readUInt32BE(20), 256);
assert.equal(
createHash("sha256").update(image).digest("hex"),
"8a989a7a2c83d66d4b10381e77bf596222f8301980518a86b4fbbcad006c1e0d",
);
});
Loading