2026-09-30 17:07:16 +02:00
|
|
|
// A private command's reply travels partly outside its sealed envelope: the
|
|
|
|
|
// status, the failure payload and the diagnostics are plaintext, and the
|
|
|
|
|
// calling app logs the diagnostics. Soul pass 4 found a provider's refresh
|
|
|
|
|
// token there: a 2xx refresh answer that is not JSON made JSON.parse quote the
|
|
|
|
|
// text around the fault, and the plane returned that message as a diagnostic.
|
|
|
|
|
// These tests drive the real Discord runtime with only fetch stubbed.
|
|
|
|
|
import { decode, encode } from "@msgpack/msgpack";
|
|
|
|
|
import { invokeCultNetOperation } from "cultnet-ts";
|
|
|
|
|
import { afterEach, expect, it, vi } from "vitest";
|
|
|
|
|
import { buildApp } from "../src/app.js";
|
|
|
|
|
import { type HeimdallConfig } from "../src/config.js";
|
|
|
|
|
import { startHeimdallPrivateCommandPlane } from "../src/private-command-plane.js";
|
|
|
|
|
import { openPrivateEnvelope, sealPrivateEnvelope, type HeimdallPrivateEnvelope } from "../src/private-command-security.js";
|
|
|
|
|
import { InMemoryStore } from "../src/store/index.js";
|
|
|
|
|
|
|
|
|
|
const secret = "ghostlight-private-command-secret";
|
|
|
|
|
const resources: Array<{ close(): Promise<unknown> }> = [];
|
|
|
|
|
afterEach(async () => {
|
|
|
|
|
vi.unstubAllGlobals();
|
|
|
|
|
await Promise.all(resources.splice(0).reverse().map((r) => r.close()));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function config(): HeimdallConfig {
|
|
|
|
|
return {
|
|
|
|
|
serviceName: "heimdall", host: "127.0.0.1", port: 4100, privateCommandHost: "127.0.0.1", privateCommandPort: 0,
|
|
|
|
|
workspaceRoot: "/tmp", dataRoot: "/tmp/.hd", cultCachePath: "/tmp/.hd/t.cc",
|
|
|
|
|
publicBaseUrl: "https://heimdall.gamecult.org", issuer: "https://heimdall.gamecult.org", daemonId: "yggdrasil-heimdall",
|
|
|
|
|
idunnRudpHealth: undefined, idunnHealthContract: "heimdall.cultnet-rudp-provider-health", providerHealthIdentityPath: "/tmp/.hd/p.cc",
|
|
|
|
|
sessionTtlSeconds: 3600, refreshTtlSeconds: 3600, stateTtlSeconds: 600, completionTtlSeconds: 300,
|
|
|
|
|
bootstrapSigningPrivateKeyOnMissing: false, tokenEncryptionKeyBase64: Buffer.alloc(32, 7).toString("base64"),
|
|
|
|
|
appSharedSecrets: { ghostlight: secret }, appRuntimeIds: { ghostlight: ["yggdrasil-ghostlight"] }, appBackendCallbacks: {},
|
|
|
|
|
storage: { backend: "memory", applySchemaOnStartup: true },
|
2026-10-19 11:13:40 +02:00
|
|
|
providers: { discord: { clientId: "discord-client", clientSecret: "discord-secret" }, patreon: {}, github: {}, twitch: {}, youtube: {}, spotify: {}, joystick: {} },
|
2026-09-30 17:07:16 +02:00
|
|
|
} as HeimdallConfig;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function command(endpoint: unknown, operation: string, contentSchema: string, key: string, payload: Record<string, unknown>) {
|
|
|
|
|
const envelope = sealPrivateEnvelope({ appSlug: "ghostlight", operation, contentSchema, idempotencyKey: key, secret, payload });
|
|
|
|
|
return invokeCultNetOperation(endpoint as never, {
|
|
|
|
|
schemaVersion: "cultnet.operation_request.v0", messageId: `m-${key}`, serviceId: "heimdall.private.commands", operation,
|
|
|
|
|
payloadSchema: "heimdall.private_command_envelope.v1", payloadEncoding: "messagepack-base64",
|
|
|
|
|
payload: Buffer.from(encode(envelope)).toString("base64"), sourceRuntimeId: "yggdrasil-ghostlight",
|
|
|
|
|
} as never, { runtimeId: "yggdrasil-ghostlight" });
|
|
|
|
|
}
|
|
|
|
|
const open = (reply: { payload: string }) =>
|
|
|
|
|
openPrivateEnvelope(decode(Buffer.from(reply.payload, "base64")) as HeimdallPrivateEnvelope, secret);
|
|
|
|
|
|
|
|
|
|
/** A signed-in Ghostlight session, then a refresh whose provider answer is `refreshAnswer`. */
|
|
|
|
|
async function refreshAgainst(refreshAnswer: () => Response) {
|
|
|
|
|
let refreshing = false;
|
|
|
|
|
vi.stubGlobal("fetch", async (url: string | URL, init?: RequestInit) => {
|
|
|
|
|
const u = String(url);
|
|
|
|
|
if (u.endsWith("/oauth2/token")) {
|
|
|
|
|
if (new URLSearchParams(String(init?.body)).get("grant_type") === "refresh_token") {
|
|
|
|
|
refreshing = true;
|
|
|
|
|
return refreshAnswer();
|
|
|
|
|
}
|
|
|
|
|
return new Response(
|
|
|
|
|
JSON.stringify({ access_token: "first-access", refresh_token: "first-refresh", token_type: "Bearer", expires_in: 60, scope: "identify" }),
|
|
|
|
|
{ status: 200 }
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if (u.endsWith("/users/@me")) return new Response(JSON.stringify({ id: "u1", username: "u1" }), { status: 200 });
|
|
|
|
|
if (u.includes("/member")) return new Response(JSON.stringify({ roles: ["r"] }), { status: 200 });
|
|
|
|
|
return new Response("{}", { status: 404 });
|
|
|
|
|
});
|
|
|
|
|
const cfg = config();
|
|
|
|
|
const app = await buildApp({ config: cfg, store: new InMemoryStore() });
|
|
|
|
|
resources.push(app);
|
|
|
|
|
const plane = await startHeimdallPrivateCommandPlane(app, cfg);
|
|
|
|
|
resources.push(plane);
|
|
|
|
|
const policy = { kind: "discord_role_access", guildId: "g", allowedRoleIds: ["r"] };
|
|
|
|
|
const begin = open(await command(plane.endpoint, "heimdall.auth.begin", "heimdall.auth_begin_command.v1", "k1",
|
|
|
|
|
{ provider: "discord", mode: "sign_in", returnTo: "https://yggdrasil.gamecult.org/ghostlight/", entitlementPolicy: policy }));
|
|
|
|
|
const state = new URL(String((begin.navigation as { url: string }).url)).searchParams.get("state")!;
|
|
|
|
|
const callback = await app.inject({
|
|
|
|
|
method: "GET",
|
|
|
|
|
url: `/v1/oauth/discord/callback?code=c1&state=${encodeURIComponent(state)}`,
|
|
|
|
|
headers: { accept: "application/json" },
|
|
|
|
|
});
|
|
|
|
|
expect(callback.statusCode, callback.body).toBe(201);
|
|
|
|
|
const done = open(await command(plane.endpoint, "heimdall.auth.complete", "heimdall.auth_complete_command.v1", "k2", { handle: begin.handle }));
|
|
|
|
|
expect(done.status).toBe("authenticated");
|
|
|
|
|
const reply = await command(plane.endpoint, "heimdall.auth.refresh", "heimdall.auth_refresh_command.v1", "k3",
|
|
|
|
|
{ refreshToken: done.refreshToken, entitlementPolicy: policy });
|
|
|
|
|
expect(refreshing).toBe(true);
|
|
|
|
|
return reply;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Every plaintext byte of a reply: the message itself and its decoded failure payload. */
|
|
|
|
|
function plaintext(reply: { payload: string }): string {
|
|
|
|
|
return JSON.stringify(reply) + JSON.stringify(decode(Buffer.from(reply.payload, "base64")));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
it("keeps a provider's malformed 2xx refresh body out of every plaintext part of the reply", async () => {
|
|
|
|
|
const reply = await refreshAgainst(
|
|
|
|
|
() => new Response('{"access_token":"CANARYaccess0123456789","refresh_token":CANARYrefresh0123456789,"token_type":"Bearer"}', { status: 200 })
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(reply.status).toBe("denied");
|
2026-10-19 11:20:18 +02:00
|
|
|
expect(reply.diagnostics).toEqual(["Heimdall denied the private command (ManagedCredentialRefreshFailed, managed_credential_refresh_failed)."]);
|
2026-09-30 17:07:16 +02:00
|
|
|
expect(plaintext(reply)).not.toContain("CANARY");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("keeps a provider's refusal body out of every plaintext part of the reply", async () => {
|
Pin provider errors and Bifrost's reply exactly; defend errorIdentity's filters
Soul pass 5, S5-4: the provider-error tests proved only that the word
CANARY was absent, so a message carrying a 16-byte slice of the body (X1,
X2) passed, and the Bifrost success reply was checked with
objectContaining, so a 9-byte body head (X5) rode along. Each provider
error's message is now pinned exactly, the Bifrost success reply is pinned
as exactly status, supportFact and bifrostStatus, and every canary body
starts with the canary, so any prefix of it shows.
S5-5: errorIdentity's class and code filters had no test (X3, X4). Cases
now show a code or class name that carries input, is too long, is not a
string, or comes from a thrown non-Error or null is dropped, and a short
identifier survives.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:57:17 +02:00
|
|
|
const reply = await refreshAgainst(() => new Response('CANARYrefused {"error":"invalid_grant"}', { status: 400 }));
|
2026-09-30 17:07:16 +02:00
|
|
|
|
|
|
|
|
expect(reply.status).toBe("denied");
|
2026-10-19 11:20:18 +02:00
|
|
|
expect(reply.diagnostics).toEqual(["Heimdall denied the private command (ManagedCredentialRefreshFailed, managed_credential_refresh_failed)."]);
|
2026-09-30 17:07:16 +02:00
|
|
|
expect(plaintext(reply)).not.toContain("CANARY");
|
|
|
|
|
});
|
2026-09-30 18:56:35 +02:00
|
|
|
|
|
|
|
|
it("keeps the completion code out of the audit trail when the plane redeems by attempt", async () => {
|
|
|
|
|
vi.stubGlobal("fetch", async (url: string | URL) => {
|
|
|
|
|
const u = String(url);
|
|
|
|
|
if (u.endsWith("/oauth2/token")) {
|
|
|
|
|
return new Response(
|
|
|
|
|
JSON.stringify({ access_token: "a", refresh_token: "r", token_type: "Bearer", expires_in: 60, scope: "identify" }),
|
|
|
|
|
{ status: 200 }
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if (u.endsWith("/users/@me")) return new Response(JSON.stringify({ id: "u1", username: "u1" }), { status: 200 });
|
|
|
|
|
if (u.includes("/member")) return new Response(JSON.stringify({ roles: ["r"] }), { status: 200 });
|
|
|
|
|
return new Response("{}", { status: 404 });
|
|
|
|
|
});
|
|
|
|
|
const cfg = config();
|
|
|
|
|
const store = new InMemoryStore();
|
|
|
|
|
const app = await buildApp({ config: cfg, store });
|
|
|
|
|
resources.push(app);
|
|
|
|
|
const plane = await startHeimdallPrivateCommandPlane(app, cfg);
|
|
|
|
|
resources.push(plane);
|
|
|
|
|
const policy = { kind: "discord_role_access", guildId: "g", allowedRoleIds: ["r"] };
|
|
|
|
|
const begin = open(await command(plane.endpoint, "heimdall.auth.begin", "heimdall.auth_begin_command.v1", "k1",
|
|
|
|
|
{ provider: "discord", mode: "sign_in", returnTo: "https://yggdrasil.gamecult.org/ghostlight/", entitlementPolicy: policy }));
|
|
|
|
|
const state = new URL(String((begin.navigation as { url: string }).url)).searchParams.get("state")!;
|
|
|
|
|
const callback = await app.inject({
|
|
|
|
|
method: "GET",
|
|
|
|
|
url: `/v1/oauth/discord/callback?code=c1&state=${encodeURIComponent(state)}`,
|
|
|
|
|
headers: { accept: "application/json" },
|
|
|
|
|
});
|
|
|
|
|
const code = (callback.json().completion as { code: string }).code;
|
|
|
|
|
const done = open(await command(plane.endpoint, "heimdall.auth.complete", "heimdall.auth_complete_command.v1", "k2", { handle: begin.handle }));
|
|
|
|
|
expect(done.status).toBe("authenticated");
|
|
|
|
|
|
|
|
|
|
const events = [...(store as unknown as { auditEvents: Map<string, { eventType: string; eventPayloadJson: unknown }> }).auditEvents.values()];
|
|
|
|
|
expect(events.filter((event) => event.eventType === "auth_completion_redeemed").map((event) => event.eventPayloadJson)).toEqual([
|
|
|
|
|
{ provider: "discord", mode: "sign_in" },
|
|
|
|
|
]);
|
|
|
|
|
expect(JSON.stringify(events)).not.toContain(code);
|
|
|
|
|
});
|