Platform
diff --git a/src/data/downloads.json b/src/data/downloads.json
index 200e491..1bbaf36 100644
--- a/src/data/downloads.json
+++ b/src/data/downloads.json
@@ -5,11 +5,13 @@
"releaseRepository": "atrinik/classic",
"artifactRole": "client",
"primary": true,
- "version": "5.8.2",
- "tag": "v5.8.2",
- "revision": "6d5932b48a019353955d393f59cdb3fea54dd45d",
- "publishedAt": "2026-08-10T00:32:43Z",
- "verifiedAt": "2026-08-10T05:09:00Z",
+ "version": "5.34.4",
+ "artifactVersion": "5.34.1",
+ "releaseCoordinateException": "authorized-artifact-version-mismatch",
+ "tag": "v5.34.4",
+ "revision": "12b1211dd44690476c3f060d7395ccc1250c85b4",
+ "publishedAt": "2026-08-23T03:18:15Z",
+ "verifiedAt": "2026-08-24T06:51:57Z",
"draft": false,
"prerelease": false,
"immutable": true,
@@ -18,17 +20,17 @@
"platform": "windows",
"architecture": "x86_64",
"archiveFormat": "zip",
- "artifact": "atrinik-classic-client-5.8.2-windows-x86_64.zip",
- "bytes": 87406395,
- "sha256": "1a7699f3ea5be50fc83b40dce8097c18cd0a892f98a2b0d462f851c2822689c0",
- "url": "https://github.com/atrinik/classic/releases/download/v5.8.2/atrinik-classic-client-5.8.2-windows-x86_64.zip",
- "releaseNotesUrl": "https://github.com/atrinik/classic/releases/tag/v5.8.2",
- "manifestUrl": "https://github.com/atrinik/classic/releases/download/v5.8.2/release-manifest.json",
- "checksumsUrl": "https://github.com/atrinik/classic/releases/download/v5.8.2/SHA256SUMS",
- "sbomUrl": "https://github.com/atrinik/classic/releases/download/v5.8.2/atrinik-classic-5.8.2.spdx.json",
+ "artifact": "atrinik-classic-client-5.34.1-windows-x86_64.zip",
+ "bytes": 87663737,
+ "sha256": "06b2dcf13d1b4fe5e024dd5d921d8102c4b7521c4fcf988e654cadd7a45f2376",
+ "url": "https://github.com/atrinik/classic/releases/download/v5.34.4/atrinik-classic-client-5.34.1-windows-x86_64.zip",
+ "releaseNotesUrl": "https://github.com/atrinik/classic/releases/tag/v5.34.4",
+ "manifestUrl": "https://github.com/atrinik/classic/releases/download/v5.34.4/release-manifest.json",
+ "checksumsUrl": "https://github.com/atrinik/classic/releases/download/v5.34.4/SHA256SUMS",
+ "sbomUrl": "https://github.com/atrinik/classic/releases/download/v5.34.4/atrinik-classic-5.34.1.spdx.json",
"softwareLicense": "GPL-2.0-or-later",
"bundledAssetsLicense": "Bundled sound and authored assets retain their exact per-file terms; inspect the packaged LICENSE.md, ATTRIBUTIONS.md, and any license files accompanying individual assets. Use the SPDX SBOM for dependency and provenance evidence.",
- "compatibility": "This client uses gameplay protocol revision 1072 and connects only to servers using the same revision; the coordinated Classic 5.8.2 server uses revision 1072.",
+ "compatibility": "This 5.34.1 client uses gameplay protocol revision 1079 and connects only to servers using the same revision; the v5.34.4 release line is temporarily serving this stable package.",
"installation": "Extract the ZIP, keep every packaged file and directory together, and run atrinik.exe from inside the extracted package."
}
]
diff --git a/src/lib/downloads.ts b/src/lib/downloads.ts
index 0a556f4..b9e3639 100644
--- a/src/lib/downloads.ts
+++ b/src/lib/downloads.ts
@@ -3,6 +3,8 @@ export interface DownloadRecord {
artifactRole: string;
primary: boolean;
version: string;
+ artifactVersion: string;
+ releaseCoordinateException: string;
tag: string;
revision: string;
publishedAt: string;
diff --git a/src/styles/global.css b/src/styles/global.css
index 3e74383..8abf0ec 100644
--- a/src/styles/global.css
+++ b/src/styles/global.css
@@ -1019,6 +1019,19 @@ main {
font-size: 1.08rem;
}
+.download-coordinate-warning {
+ margin-top: 1rem;
+ margin-bottom: 0;
+ padding: 1rem 1.2rem;
+ border: 1px solid var(--line-gold);
+ background: rgba(114, 221, 255, 0.08);
+ color: var(--text-soft);
+}
+
+.download-coordinate-warning strong {
+ color: var(--text);
+}
+
.download-facts {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
diff --git a/tools/fixtures/download-adversarial-catalog.json b/tools/fixtures/download-adversarial-catalog.json
index 9fef8d2..ca10d32 100644
--- a/tools/fixtures/download-adversarial-catalog.json
+++ b/tools/fixtures/download-adversarial-catalog.json
@@ -6,6 +6,8 @@
"artifactRole": "client",
"primary": true,
"version": "5.9.0",
+ "artifactVersion": "5.9.0",
+ "releaseCoordinateException": "none",
"tag": "v5.9.0",
"revision": "0123456789abcdef0123456789abcdef01234567",
"publishedAt": "2026-08-10T01:00:00Z",
diff --git a/tools/fixtures/download-valid.json b/tools/fixtures/download-valid.json
index 428bca7..1b6a8d6 100644
--- a/tools/fixtures/download-valid.json
+++ b/tools/fixtures/download-valid.json
@@ -3,6 +3,8 @@
"artifactRole": "client",
"primary": true,
"version": "1.2.3",
+ "artifactVersion": "1.2.3",
+ "releaseCoordinateException": "none",
"tag": "v1.2.3",
"revision": "0123456789abcdef0123456789abcdef01234567",
"publishedAt": "2026-01-02T03:04:05Z",
diff --git a/tools/site-contract.mjs b/tools/site-contract.mjs
index 1d79592..a7aa4c3 100644
--- a/tools/site-contract.mjs
+++ b/tools/site-contract.mjs
@@ -24,6 +24,14 @@ const versionPattern = /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/u;
const releaseRepositoryPattern = /^atrinik\/[a-z][a-z0-9-]*$/u;
const artifactRolePattern = /^[a-z][a-z0-9-]*$/u;
const artifactPattern = /^[A-Za-z0-9][A-Za-z0-9._-]+$/u;
+const releaseCoordinateExceptionValues = new Set([
+ "none",
+ "authorized-artifact-version-mismatch",
+]);
+const authorizedReleaseCoordinateException = Object.freeze({
+ releaseVersion: "5.34.4",
+ artifactVersion: "5.34.1",
+});
const utcTimestampPattern =
/^(?:(?:\d{2}(?:0[48]|[2468][048]|[13579][26])|(?:[02468][048]|[13579][26])00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|02-(?:0[1-9]|1\d|2[0-8])))T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\dZ$/u;
const mediaPathPattern = /^\/media\/[a-z0-9][a-z0-9-]*\.([0-9a-f]{8})\.webp$/u;
@@ -89,6 +97,8 @@ export function validateDownload(record) {
"artifactRole",
"primary",
"version",
+ "artifactVersion",
+ "releaseCoordinateException",
"tag",
"revision",
"publishedAt",
@@ -127,6 +137,26 @@ export function validateDownload(record) {
record.tag !== `v${record.version}`
)
throw new Error("download version/tag mismatch");
+ if (
+ !versionPattern.test(record.artifactVersion) ||
+ !releaseCoordinateExceptionValues.has(record.releaseCoordinateException)
+ )
+ throw new Error("invalid download artifact version binding");
+ const artifactVersionMismatch = record.artifactVersion !== record.version;
+ const authorizedArtifactVersionMismatch =
+ record.primary === true &&
+ record.version === authorizedReleaseCoordinateException.releaseVersion &&
+ record.artifactVersion ===
+ authorizedReleaseCoordinateException.artifactVersion &&
+ record.releaseCoordinateException ===
+ "authorized-artifact-version-mismatch";
+ if (
+ (artifactVersionMismatch && !authorizedArtifactVersionMismatch) ||
+ (!artifactVersionMismatch && record.releaseCoordinateException !== "none")
+ )
+ throw new Error(
+ "download artifact version mismatch is not explicitly authorized",
+ );
if (
!revisionPattern.test(record.revision) ||
!sha256Pattern.test(record.sha256)
@@ -227,9 +257,9 @@ export function validateDownloadCatalog(catalog) {
record.archiveFormat !== "zip" ||
record.releaseAssets !== 12 ||
record.artifact !==
- `atrinik-classic-client-${record.version}-windows-x86_64.zip` ||
+ `atrinik-classic-client-${record.artifactVersion}-windows-x86_64.zip` ||
record.sbomUrl !==
- `https://github.com/atrinik/classic/releases/download/${record.tag}/atrinik-classic-${record.version}.spdx.json` ||
+ `https://github.com/atrinik/classic/releases/download/${record.tag}/atrinik-classic-${record.artifactVersion}.spdx.json` ||
record.softwareLicense !== "GPL-2.0-or-later",
)
)
@@ -243,7 +273,10 @@ export function validateDownloadSchemaDefinition(schema, fixture) {
schema?.additionalProperties !== false ||
schema.required?.toSorted().join("\n") !== expectedFields ||
properties.version?.pattern !== versionPatternSource ||
+ properties.artifactVersion?.pattern !== versionPatternSource ||
properties.tag?.pattern !== tagPatternSource ||
+ properties.releaseCoordinateException?.enum?.join("\n") !==
+ "none\nauthorized-artifact-version-mismatch" ||
new RegExp(properties.releaseRepository?.pattern ?? "").source !==
releaseRepositoryPattern.source ||
new RegExp(properties.artifactRole?.pattern ?? "").source !==
@@ -335,6 +368,7 @@ export function validateDownloadsPresentation(html, catalog) {
}
const requiredEvidence = [
primary.version,
+ primary.artifactVersion,
primary.revision,
primary.sha256,
primary.artifact,
@@ -353,12 +387,22 @@ export function validateDownloadsPresentation(html, catalog) {
throw new Error("primary download presentation omits catalog evidence");
const structuralEvidence = [
`Platform${primary.platform === "windows" ? "Windows" : primary.platform} ${primary.architecture}`,
+ `
Artifact version${primary.artifactVersion}`,
`
Archive${primary.archiveFormat.toUpperCase()}`,
`
Install and compatibility
${primary.installation}
${primary.compatibility}
`,
`
License boundary
Classic software is licensed under ${primary.softwareLicense}. ${primary.bundledAssetsLicense}
`,
];
if (structuralEvidence.some((value) => !html.includes(value)))
throw new Error("primary download presentation misplaces catalog evidence");
+ if (
+ primary.releaseCoordinateException !== "none" &&
+ !html.includes(
+ `Temporary release-coordinate exception: the ${primary.tag} release coordinate currently serves a Classic client package labeled v${primary.artifactVersion}.`,
+ )
+ )
+ throw new Error(
+ "primary download presentation omits its release exception",
+ );
if (
downloadReleaseUrls(primary).some(
(url) => !html.includes(`href="${url}"`),
diff --git a/tools/site-contract.test.mjs b/tools/site-contract.test.mjs
index e7f2401..bd11dc2 100644
--- a/tools/site-contract.test.mjs
+++ b/tools/site-contract.test.mjs
@@ -177,6 +177,51 @@ test("download coordinates are closed and immutable", () => {
validateDownloadSchemaDefinition(downloadSchema, validDownload),
);
assert.doesNotThrow(() => validateDownload(validDownload));
+ const authorizedArtifactMismatch = {
+ ...validDownload,
+ version: "5.34.4",
+ artifactVersion: "5.34.1",
+ releaseCoordinateException: "authorized-artifact-version-mismatch",
+ tag: "v5.34.4",
+ artifact: "atrinik-classic-client-5.34.1-windows-x86_64.zip",
+ url: "https://github.com/atrinik/classic/releases/download/v5.34.4/atrinik-classic-client-5.34.1-windows-x86_64.zip",
+ releaseNotesUrl: "https://github.com/atrinik/classic/releases/tag/v5.34.4",
+ manifestUrl:
+ "https://github.com/atrinik/classic/releases/download/v5.34.4/release-manifest.json",
+ checksumsUrl:
+ "https://github.com/atrinik/classic/releases/download/v5.34.4/SHA256SUMS",
+ sbomUrl:
+ "https://github.com/atrinik/classic/releases/download/v5.34.4/atrinik-classic-5.34.1.spdx.json",
+ };
+ assert.doesNotThrow(() => validateDownload(authorizedArtifactMismatch));
+ assert.throws(
+ () =>
+ validateDownload({
+ ...authorizedArtifactMismatch,
+ artifactVersion: "5.34.2",
+ artifact: "atrinik-classic-client-5.34.2-windows-x86_64.zip",
+ url: "https://github.com/atrinik/classic/releases/download/v5.34.4/atrinik-classic-client-5.34.2-windows-x86_64.zip",
+ sbomUrl:
+ "https://github.com/atrinik/classic/releases/download/v5.34.4/atrinik-classic-5.34.2.spdx.json",
+ }),
+ /explicitly authorized/u,
+ );
+ assert.throws(
+ () =>
+ validateDownload({
+ ...validDownload,
+ releaseCoordinateException: "authorized-artifact-version-mismatch",
+ }),
+ /explicitly authorized/u,
+ );
+ assert.throws(
+ () =>
+ validateDownload({
+ ...authorizedArtifactMismatch,
+ primary: false,
+ }),
+ /explicitly authorized/u,
+ );
assert.throws(
() =>
validateDownload({ ...validDownload, url: "https://example.com/latest" }),
@@ -240,6 +285,8 @@ test("download coordinates are closed and immutable", () => {
test("download schema and executable constraints reject the same edge forms", () => {
const rejected = [
["version", "01.2.3"],
+ ["artifactVersion", "01.2.3"],
+ ["releaseCoordinateException", "unreviewed-mismatch"],
["tag", "v01.2.3"],
["publishedAt", "2026-99-99T99:99:99Z"],
["publishedAt", "2026-02-31T00:00:00Z"],
@@ -359,6 +406,7 @@ test("download catalogs stay empty safely and reject ineligible releases", () =>
test("download presentation structurally preserves all catalog evidence", () => {
const evidence = [
validDownload.version,
+ validDownload.artifactVersion,
validDownload.revision,
validDownload.sha256,
validDownload.artifact,
@@ -385,6 +433,7 @@ test("download presentation structurally preserves all catalog evidence", () =>
.join(" ");
const structures = [
`
PlatformWindows ${validDownload.architecture}`,
+ `
Artifact version${validDownload.artifactVersion}`,
`
Archive${validDownload.archiveFormat.toUpperCase()}`,
`
Install and compatibility
${validDownload.installation}
${validDownload.compatibility}
`,
`
License boundary
Classic software is licensed under ${validDownload.softwareLicense}. ${validDownload.bundledAssetsLicense}
`,