cultnet-ts: Idunn authority files are read by cultcache-ts's one store reader

readAuthorityRecord decoded the store itself and accepted only cultcache.store.v1. It now calls
readSingleFileStore, so v3 reads, a zero-byte file is a StoreUnreadableError, and a refused
header is never echoed. Tests derive their bundles from the Rust-written fixtures.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-02 22:12:15 +02:00
commit 8a833730c9
2 changed files with 58 additions and 22 deletions

View file

@ -1,6 +1,7 @@
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { readSingleFileStore } from "@gamecult/cultcache-ts";
import { decode, encode } from "@msgpack/msgpack";
import {
@ -27,7 +28,6 @@ export const IDUNN_RUNTIME_CANDIDATE_BIND_ENVIRONMENT = "GAMECULT_IDUNN_CANDIDAT
const PROVIDER_ID_DOMAIN = Buffer.from("gamecult.provider-health.identity.v1\0", "utf8");
const PROVIDER_PROTECTOR_CONTEXT = "gamecult-provider-health-identity-v1";
const ACTIVATION_ID_DOMAIN = Buffer.from("idunn.runtime-activation.id.v1\0", "utf8");
const CULTCACHE_STORE_FORMAT = "cultcache.store.v1";
const ED25519_PKCS8_PREFIX = Buffer.from("302e020100300506032b657004220420", "hex");
// Limit: more than this many warmings signed between Idunn's observation and
// assertWriteLease would push the leased one out and refuse a valid lease.
@ -318,25 +318,13 @@ function deepFreeze<T>(value: T): T {
}
function readAuthorityRecord(filePath: string, type: string, schemaId: string): { payload: Uint8Array } {
const decoded = decode(fs.readFileSync(filePath));
if (!Array.isArray(decoded) || decoded[0] !== CULTCACHE_STORE_FORMAT || !Array.isArray(decoded[1]) || !Array.isArray(decoded[2])) {
throw new Error(`Idunn authority file ${filePath} is not a CultCache v1 store.`);
}
if (decoded[2].length !== 1) throw new Error(`Idunn authority file ${filePath} must contain exactly one record.`);
const matchingRecords = (decoded[2] as unknown[]).filter((raw) => {
const row = array(raw, 4, "CultCache record");
return row[1] === schemaId;
});
if (matchingRecords.length !== 1) throw new Error(`Idunn authority file must contain exactly one ${schemaId} record.`);
const row = array(matchingRecords[0], 4, "CultCache record");
const catalog = (decoded[1] as unknown[]).find((raw) => {
const entry = Array.isArray(raw) ? raw : [];
return entry[0] === schemaId;
});
const catalogEntry = array(catalog, 7, "CultCache schema catalog entry");
if (catalogEntry[1] !== type) throw new Error(`Idunn authority record has unexpected CultCache type ${String(catalogEntry[1])}.`);
if (!(row[3] instanceof Uint8Array)) throw new Error("Idunn authority payload is not MessagePack bytes.");
return { payload: row[3] };
const store = readSingleFileStore(filePath);
if (store.absent) throw new Error(`Idunn authority file ${filePath} does not exist.`);
if (store.envelopes.length !== 1) throw new Error(`Idunn authority file ${filePath} must contain exactly one record.`);
const record = store.envelopes[0]!;
if (record.schemaId !== schemaId) throw new Error(`Idunn authority file must contain exactly one ${schemaId} record.`);
if (record.type !== type) throw new Error(`Idunn authority record has unexpected CultCache type ${record.type}.`);
return { payload: record.payload };
}
function decodeExpected(payload: Uint8Array): ExpectedIncarnation {

View file

@ -3,7 +3,9 @@ import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import dgram from "node:dgram";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { StoreUnreadableError } from "@gamecult/cultcache-ts";
import { decode, encode } from "@msgpack/msgpack";
import {
@ -54,7 +56,7 @@ function expectedFacts(set: FixtureSet) {
function openAuthority(
context: TestContext,
set: FixtureSet,
options: { machineId?: "etc" | "dbus"; lease?: string } = {},
options: { machineId?: "etc" | "dbus"; lease?: string; bundle?: string } = {},
): IdunnRuntimeAuthority {
const facts = expectedFacts(set);
const originalEnvironment = { ...process.env };
@ -62,7 +64,7 @@ function openAuthority(
for (const key of Object.keys(process.env)) if (!(key in originalEnvironment)) delete process.env[key];
Object.assign(process.env, originalEnvironment);
});
process.env.GAMECULT_IDUNN_RUNTIME_BUNDLE = path.join(FIXTURES, set);
process.env.GAMECULT_IDUNN_RUNTIME_BUNDLE = options.bundle ?? path.join(FIXTURES, set);
process.env.GAMECULT_IDUNN_CANDIDATE_BIND = facts.candidate;
if (options.lease) process.env.GAMECULT_IDUNN_PROCESS_WRITE_LEASE = path.join(FIXTURES, set, options.lease);
else delete process.env.GAMECULT_IDUNN_PROCESS_WRITE_LEASE;
@ -84,6 +86,22 @@ function openAuthority(
return loadIdunnRuntimeAuthorityFromEnvironment(facts.target, facts.healthContract);
}
/** A copy of one Rust-written bundle in a scratch directory, with `change` applied to the bytes of the named file. */
function bundleWith(context: TestContext, set: FixtureSet, file: string, change: (bytes: Buffer) => Buffer | undefined): string {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "idunn-authority-"));
context.after(() => fs.rmSync(dir, { recursive: true, force: true }));
for (const name of ["expected.cc", "activation.cc"]) {
const bytes = fixtureFile(set, name);
const changed = name === file ? change(bytes) : bytes;
if (changed !== undefined) fs.writeFileSync(path.join(dir, name), changed);
}
return dir;
}
function withHeader(header: string): (bytes: Buffer) => Buffer {
return (bytes) => Buffer.from(encode([header, ...(decode(bytes) as unknown[]).slice(1)]));
}
function signerFor(context: TestContext, set: FixtureSet, capacityDelta = 0, lease?: string) {
const authority = openAuthority(context, set, { lease });
const capabilities = expectedFacts(set).capabilities.map((capability) => ({ ...capability, capacity: capability.capacity + capacityDelta }));
@ -133,6 +151,36 @@ test("rejects a credential file holding more than one envelope", (context) => {
);
});
// The authority files are read by cultcache-ts's one store reader, so a store it reads is read here and a file it
// refuses is refused here with the same typed error and the same text.
for (const file of ["expected.cc", "activation.cc"]) {
test(`${file}: a store marked for element ids opens, as the one reader reads it`, (context) => {
const bundle = bundleWith(context, "web", file, withHeader("cultcache.store.v3"));
assert.equal(openAuthority(context, "web", { bundle }).expected.target, expectedFacts("web").target);
});
test(`${file}: a zero-byte file is refused as unreadable, not read as an empty store or a missing one`, (context) => {
const bundle = bundleWith(context, "web", file, () => Buffer.alloc(0));
assert.throws(() => openAuthority(context, "web", { bundle }), StoreUnreadableError);
});
test(`${file}: a file that is not there is refused`, (context) => {
const bundle = bundleWith(context, "web", file, () => undefined);
assert.throws(() => openAuthority(context, "web", { bundle }), /does not exist/);
});
test(`${file}: a refused header is never echoed, whether or not it starts at the store prefix`, (context) => {
for (const header of ["cultcache.store.SECRET", "cultcache.store.SECRETcultcache.store.v9", "SECRETcultcache.store.v9", "cultcache.store.v9"]) {
const bundle = bundleWith(context, "web", file, withHeader(header));
assert.throws(
() => openAuthority(context, "web", { bundle }),
(error: Error) => error instanceof StoreUnreadableError && (header === "cultcache.store.v9" ? error.message.includes(header) : !error.message.includes("SECRET")),
header,
);
}
});
}
function presenceSlot(document: { payload: Uint8Array }, slot: number): unknown {
return (decode(document.payload) as unknown[])[slot];
}