Heimdall/tests/security.test.ts
Meta Vens 1c38d12050 Add Joystick as a confidential, rotating managed-connection provider
Joystick joins the catalog (Basic client auth, rotatesRefreshTokens), resolves
identity by channel id, and is a streampixels managed-connection provider only:
it is not an identity provider for the profile (ruling signin-waits-for-public-bot).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 11:13:40 +02:00

122 lines
4.1 KiB
TypeScript

import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { afterEach, describe, expect, it } from "vitest";
import { type HeimdallConfig } from "../src/config.js";
import { createTokenCustody } from "../src/custody.js";
import { createRuntimeKeyMaterial } from "../src/signing.js";
function createTestConfig(overrides: Partial<HeimdallConfig> = {}): HeimdallConfig {
return {
serviceName: "heimdall",
host: "127.0.0.1",
port: 4100,
workspaceRoot: "E:/Projects/Heimdall",
dataRoot: "E:/Projects/Heimdall/.heimdall-data",
cultCachePath: "E:/Projects/Heimdall/.heimdall-data/cultcache/heimdall.service.cc",
publicBaseUrl: "https://heimdall.gamecult.org",
issuer: "https://heimdall.gamecult.org",
daemonId: "yggdrasil-heimdall",
idunnRudpHealth: undefined,
idunnHealthContract: "heimdall.cultnet-rudp-provider-health",
providerHealthIdentityPath: "E:/Projects/Heimdall/.heimdall-data/provider-health-identity.cc",
sessionTtlSeconds: 3600,
refreshTtlSeconds: 60 * 60 * 24 * 30,
stateTtlSeconds: 600,
completionTtlSeconds: 300,
bootstrapSigningPrivateKeyOnMissing: false,
appSharedSecrets: {},
appBackendCallbacks: {},
storage: {
backend: "memory",
applySchemaOnStartup: true,
},
providers: {
discord: {},
patreon: {},
github: {},
twitch: {},
youtube: {},
spotify: {},
joystick: {},
},
...overrides,
};
}
const tempDirs: string[] = [];
afterEach(() => {
while (tempDirs.length) {
rmSync(tempDirs.pop() ?? "", { force: true, recursive: true });
}
});
describe("Heimdall security helpers", () => {
it("round-trips provider tokens through AES-GCM custody", () => {
const custody = createTokenCustody({
...createTestConfig(),
tokenEncryptionKeyBase64: Buffer.alloc(32, 23).toString("base64"),
});
const encrypted = custody.encrypt("discord-access-token");
expect(encrypted).toMatch(/^gc_tok_v1:/);
expect(encrypted).not.toBe("discord-access-token");
expect(custody.decrypt(encrypted)).toBe("discord-access-token");
});
it("refuses a token key that does not decode to 32 bytes, without echoing it", () => {
for (const length of [16, 31, 33]) {
const key = Buffer.alloc(length, 7).toString("base64");
const message = (() => {
try {
createTokenCustody({ ...createTestConfig(), tokenEncryptionKeyBase64: key });
} catch (error) {
return (error as Error).message;
}
throw new Error(`a ${length}-byte key was accepted`);
})();
expect(message).toMatch(/GC_ACCESS_TOKEN_ENCRYPTION_KEY_BASE64_FILE must decode to exactly 32 bytes/);
expect(message).not.toContain(key);
}
});
it("refuses persistent provider-token storage without a stable encryption key", () => {
expect(() =>
createTokenCustody(
createTestConfig({
storage: {
backend: "postgres",
applySchemaOnStartup: true,
databaseUrl: "postgres://127.0.0.1/heimdall",
},
})
)
).toThrow(/GC_ACCESS_TOKEN_ENCRYPTION_KEY_BASE64/);
});
it("bootstraps and then reuses a file-backed signing key", () => {
const tempDir = mkdtempSync(join(tmpdir(), "heimdall-signing-"));
tempDirs.push(tempDir);
const signingKeyPath = join(tempDir, "keys", "heimdall-signing-key.pem");
const first = createRuntimeKeyMaterial(
createTestConfig({
signingPrivateKeyPath: signingKeyPath,
bootstrapSigningPrivateKeyOnMissing: true,
})
);
const second = createRuntimeKeyMaterial(
createTestConfig({
signingPrivateKeyPath: signingKeyPath,
bootstrapSigningPrivateKeyOnMissing: false,
})
);
expect(first.source).toBe("bootstrapped_file");
expect(second.source).toBe("configured_file");
expect(first.kid).toBe(second.kid);
expect(existsSync(signingKeyPath)).toBe(true);
expect(readFileSync(signingKeyPath, "utf8")).toContain("BEGIN PRIVATE KEY");
});
});