diff --git a/src/idunn-rudp-health.ts b/src/idunn-rudp-health.ts index d9c3c01..135476e 100644 --- a/src/idunn-rudp-health.ts +++ b/src/idunn-rudp-health.ts @@ -1,7 +1,7 @@ import dgram from "node:dgram"; -import { randomUUID } from "node:crypto"; import { encode } from "@msgpack/msgpack"; import { + GAMECULT_RUNTIME_PRESENCE_HEALTH_SCHEMA, CultNetRudpSession, decodeRudpPacket, encodeCultNetMessageForWire, @@ -10,19 +10,13 @@ import { type CultNetRudpPacket, } from "cultnet-ts"; import type { HeimdallConfig } from "./config.js"; -import { - openProviderHealthIdentity, - signProviderHealthPayload, -} from "./provider-health-identity.js"; +import type { PresenceStatement } from "./runtime-presence.js"; const CULTNET_RUDP_PROTOCOL_ID = "cultnet.transport.rudp.v0"; const IDUNN_HEALTH_RUDP_CONNECTION_ID = 0x1d0d0001; const RUDP_HEALTH_CONNECT_ATTEMPTS = 3; const RUDP_ACCEPT_TIMEOUT_MS = 2_000; const RUDP_ACK_TIMEOUT_MS = 1_000; -const SIGNED_DAEMON_HEALTH_SCHEMA = "idunn.signed_daemon_health.v1"; -const publisherIncarnationId = randomUUID(); -let publisherSequence = 0; type Endpoint = { host: string; @@ -34,6 +28,8 @@ type IdunnHealthInput = { state: string; detail: string; observedAt: string; + /** The dual-proved statement, already built and signed. */ + presence: PresenceStatement; }; export async function publishIdunnRudpHealth(config: HeimdallConfig, health: IdunnHealthInput): Promise { @@ -91,42 +87,25 @@ async function publishIdunnRudpHealthOnce( } } -async function buildSignedDocumentPutPayload(config: HeimdallConfig, health: IdunnHealthInput): Promise { - const identity = await openProviderHealthIdentity(config.providerHealthIdentityPath); - publisherSequence += 1; - const unsigned = [ - SIGNED_DAEMON_HEALTH_SCHEMA, - health.daemonId, - config.idunnHealthContract, - "heimdall-service", - health.state, - health.detail, - identity.identityId, - publisherIncarnationId, - publisherSequence, - Date.parse(health.observedAt), - null, - null, - null, - null, - "ed25519", - new Uint8Array(), - false, - ]; - const signature = signProviderHealthPayload(identity, encode(unsigned)); - const recordPayload = encode([...unsigned.slice(0, 15), signature, false]); +async function buildSignedDocumentPutPayload( + config: HeimdallConfig, + health: IdunnHealthInput, +): Promise { + // The record key must equal the target the statement signs. Odin refuses the + // pair when they differ, and it is the one part of the envelope the transport + // cannot infer from the payload without decoding it. const message: CultNetDocumentPutRawMessage = { schemaVersion: "cultnet.document_put_raw.v0", - messageId: `heimdall-health:${health.daemonId}:${health.observedAt.replace(/[:.]/g, "-")}`, + messageId: `heimdall-presence:${health.presence.target}:${health.observedAt.replace(/[:.]/g, "-")}`, document: { - schemaId: SIGNED_DAEMON_HEALTH_SCHEMA, - recordKey: health.daemonId, + schemaId: GAMECULT_RUNTIME_PRESENCE_HEALTH_SCHEMA, + recordKey: health.presence.target, storedAt: health.observedAt, payloadEncoding: "messagepack", - payload: recordPayload, - sourceRuntimeId: "heimdall-service", - sourceAgentId: identity.identityId, - sourceRole: "daemon-health-publisher", + payload: health.presence.payload, + sourceRuntimeId: config.idunnHealthContract, + sourceAgentId: health.presence.target, + sourceRole: "runtime-presence-publisher", tags: [CULTNET_RUDP_PROTOCOL_ID], }, }; diff --git a/src/index.ts b/src/index.ts index 8c34e86..4c89a70 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ +import path from "node:path"; import { resolveWriteLease, WriteLeaseNotHeldError } from "./process-write-lease.js"; import { buildApp } from "./app.js"; import { loadConfig } from "./config.js"; @@ -6,6 +7,12 @@ import { publishHeimdallOdinState } from "./odin-publication.js"; import { startHeimdallPrivateCommandPlane } from "./private-command-plane.js"; import type { CultNetOperationServer } from "cultnet-ts"; import { createHeimdallRuntimePulse, buildHeimdallHealthDetail, publishHeimdallVerseState } from "./verse-state.js"; +import { + buildPresenceStatement, + nextPublisherSequence, + readRuntimeBundleFacts, + type RuntimeBundleFacts, +} from "./runtime-presence.js"; const config = loadConfig(); const app = await buildApp({ config }); @@ -63,6 +70,13 @@ async function publishVerseState(): Promise { await publishHeimdallOdinState(config, pulse); } +/** + * Read once and keep: the bundle is Idunn's immutable record of this launch and + * cannot change while the process lives, so re-reading it every pulse would be + * work that can only ever return the same answer or fail. + */ +let runtimeBundleFacts: RuntimeBundleFacts | undefined; + async function publishHealthState(): Promise { const pulse = createHeimdallRuntimePulse(config); @@ -80,11 +94,35 @@ async function publishHealthState(): Promise { ? buildHeimdallHealthDetail(config, pulse) : `Heimdall candidate warming; ${lease.reason}`; + if (!config.idunnRuntimeBundlePath) { + // Outside Idunn there is no launch to attest and nothing that admits the + // statement; publishing is simply not part of that world. + return; + } + runtimeBundleFacts ??= await readRuntimeBundleFacts(config.idunnRuntimeBundlePath); + + const observedAtUnixMillis = Date.parse(pulse.updatedAt); + const presence = await buildPresenceStatement({ + bundle: runtimeBundleFacts, + state: lease.mayWrite ? "active" : "warming", + detail, + boundEndpoint: `http://${config.host}:${config.port}`, + // A warming candidate holds no lease and must not claim one; the contract + // refuses the pair, and Idunn needs the warming statement to promote it. + writeLeaseSha256: null, + observedAtUnixMillis, + publisherSequence: await nextPublisherSequence( + path.join(config.dataRoot, "runtime-presence-sequence"), + ), + providerHealthIdentityPath: config.providerHealthIdentityPath, + }); + await publishIdunnRudpHealth(config, { daemonId: config.daemonId, state: lease.mayWrite ? "active" : "warming", detail, observedAt: pulse.updatedAt, + presence, }); } diff --git a/src/provider-health-identity.ts b/src/provider-health-identity.ts index a4a4c64..16e5826 100644 --- a/src/provider-health-identity.ts +++ b/src/provider-health-identity.ts @@ -20,7 +20,7 @@ const SIGNING_PURPOSE = Buffer.from("idunn.signed_daemon_health.v1"); const PROTECTOR_CONTEXT = "gamecult-provider-health-identity-v1"; const PKCS8_ED25519_SEED_PREFIX = Buffer.from("302e020100300506032b657004220420", "hex"); -type PrivateIdentity = { +export type PrivateIdentity = { identityId: string; publicKey: Uint8Array; privateKey: KeyObject; @@ -87,22 +87,24 @@ const SYSTEMD_LISTEN_FDS_START = 3; * cannot open the 0600 root-owned store, and a descriptor cannot be re-read by * anything that did not inherit it. */ -function presenceIdentityDescriptor( - env: NodeJS.ProcessEnv, - pid: number +export function passedDescriptor( + descriptorName: string, + env: NodeJS.ProcessEnv ): number | undefined { const names = env.LISTEN_FDNAMES; if (!names) { return undefined; } - // LISTEN_PID guards against consuming descriptors meant for a parent or a - // sibling; systemd sets it to the intended recipient. - if (env.LISTEN_PID && Number(env.LISTEN_PID) !== pid) { - return undefined; - } - - const index = names.split(":").indexOf(PRESENCE_IDENTITY_FD_NAME); + // LISTEN_PID cannot be compared to our own pid. Idunn launches candidates + // with PrivatePIDs=yes, and systemd sets LISTEN_PID to the pid it knows, + // which is in the *outer* namespace; inside, process.pid is the + // namespace-local one. The comparison could never hold, and because this + // function answers "undefined" rather than throwing, the caller quietly + // enrolled a self-signed identity instead -- exactly the failure the next + // doc comment warns about, and one that shows up only as health nobody + // admits. What identifies the descriptor is its name in LISTEN_FDNAMES. + const index = names.split(":").indexOf(descriptorName); return index === -1 ? undefined : SYSTEMD_LISTEN_FDS_START + index; } @@ -122,10 +124,9 @@ function presenceIdentityDescriptor( */ export async function openProviderHealthIdentity( path: string, - env: NodeJS.ProcessEnv = process.env, - pid: number = process.pid + env: NodeJS.ProcessEnv = process.env ): Promise { - const descriptor = presenceIdentityDescriptor(env, pid); + const descriptor = passedDescriptor(PRESENCE_IDENTITY_FD_NAME, env); if (descriptor === undefined) { return openOrEnrollProviderHealthIdentity(path); } @@ -149,6 +150,17 @@ export function signProviderHealthPayload(identity: PrivateIdentity, payload: Ui return sign(null, message, identity.privateKey); } +/** + * Sign an already domain-separated message with this identity. + * + * The domain and purpose belong to the contract being signed, not to the key, + * so they are built by the caller from the shared CultNet helpers rather than + * assumed here. + */ +export function signWithIdentity(identity: PrivateIdentity, message: Buffer): Uint8Array { + return sign(null, message, identity.privateKey); +} + export function providerHealthPublicIdentity(identity: PrivateIdentity): { identityId: string; publicKeyHex: string; diff --git a/src/runtime-presence.ts b/src/runtime-presence.ts new file mode 100644 index 0000000..8cf0615 --- /dev/null +++ b/src/runtime-presence.ts @@ -0,0 +1,306 @@ +/** + * Heimdall's runtime presence statement. + * + * Odin admits a document as runtime presence only when it carries the + * `gamecult.runtime_presence_health.v2` schema and its record key equals the + * signed target. Anything else it stores as an ordinary document and never + * correlates, so a candidate publishing the wrong contract warms forever + * without ever being promoted — healthy from every angle except the one that + * decides. + * + * The statement is dual-proved. The stable provider-health key says "Heimdall + * made this statement"; the activation key Idunn passed at launch says "and it + * is the incarnation Idunn started". Both cover one payload, and neither + * establishes admission alone. Every field describing the launch is read from + * the runtime bundle rather than from configuration: the bundle is Idunn's own + * immutable record of what it started, and a statement assembled from anything + * else would be describing a launch nobody authorised. + */ + +import path from "node:path"; +import { createHash, createPrivateKey, type KeyObject } from "node:crypto"; +import { readFileSync } from "node:fs"; +import { readFile, writeFile, rename } from "node:fs/promises"; + +import { decode } from "@msgpack/msgpack"; +import { + IDUNN_RUNTIME_ACTIVATION_CREDENTIAL_NAME, + encodeRuntimePresenceHealth, + runtimePresenceActivationSigningMessage, + runtimePresenceProofPayload, + runtimePresenceProviderSigningMessage, + type GameCultRuntimeCapability, + type RuntimePresenceHealth, + type RuntimePresenceState, +} from "cultnet-ts"; + +import { readIdunnStoreRecord } from "./idunn-store.js"; +import { + openProviderHealthIdentity, + passedDescriptor, + signWithIdentity, +} from "./provider-health-identity.js"; + +const PKCS8_ED25519_SEED_PREFIX = Buffer.from("302e020100300506032b657004220420", "hex"); + +/** Positional slots in `idunn.expected_incarnation.v2`. */ +const EXPECTED = { + target: 1, + planId: 2, + incarnationId: 3, + sealedReleaseId: 4, + runtimeId: 8, + healthContract: 10, + stateSchemaGeneration: 12, + stateContractSha256: 13, + capabilities: 16, +} as const; + +/** Positional slots in `idunn.runtime_activation.v2`. */ +const ACTIVATION = { + expectedProjectionSha256: 1, + runtimeInstanceId: 3, + activationSignerIdentityId: 4, +} as const; + +export interface RuntimeBundleFacts { + target: string; + planId: string; + incarnationId: string; + sealedReleaseId: string; + runtimeId: string; + runtimeInstanceId: string; + expectedProjectionSha256: string; + activationWitnessSha256: string; + activationSignerIdentityId: string; + stateSchemaGeneration: string | null; + stateContractSha256: string | null; + capabilities: GameCultRuntimeCapability[]; + healthContract: string; +} + +function fields(payload: Uint8Array, label: string): unknown[] { + const decoded = decode(payload); + if (!Array.isArray(decoded)) { + throw new Error(`${label} is not positional MessagePack`); + } + return decoded; +} + +function text(values: unknown[], index: number, label: string): string { + const value = values[index]; + if (typeof value !== "string" || value.length === 0) { + throw new Error(`${label} field ${index} is not a non-empty string`); + } + return value; +} + +function optionalText(values: unknown[], index: number): string | null { + const value = values[index]; + return typeof value === "string" && value.length > 0 ? value : null; +} + +function capabilities(values: unknown[], index: number): GameCultRuntimeCapability[] { + const claims = values[index]; + if (claims === undefined || claims === null) { + return []; + } + if (!Array.isArray(claims)) { + throw new Error("expected incarnation capabilities are not a list"); + } + return claims.map((claim) => { + if (!Array.isArray(claim) || claim.length < 4) { + throw new Error("expected incarnation capability is not a positional tuple"); + } + const [capability, schema, compatibility, capacity] = claim as unknown[]; + if ( + typeof capability !== "string" || + typeof schema !== "string" || + typeof compatibility !== "string" || + typeof capacity !== "number" + ) { + throw new Error("expected incarnation capability has an unexpected shape"); + } + return { capability, schema, compatibility, capacity }; + }); +} + +/** + * Read what Idunn recorded about this launch. + * + * `expected.cc` describes the incarnation Idunn intends; `activation.cc` names + * the process it actually started and the activation signer whose proof this + * statement must carry. + */ +export async function readRuntimeBundleFacts(bundlePath: string): Promise { + const expected = fields( + await readIdunnStoreRecord(path.join(bundlePath, "expected.cc"), "expected incarnation"), + "expected incarnation", + ); + const activationPayload = await readIdunnStoreRecord( + path.join(bundlePath, "activation.cc"), + "runtime activation", + ); + const activation = fields(activationPayload, "runtime activation"); + + const generation = optionalText(expected, EXPECTED.stateSchemaGeneration); + const contract = optionalText(expected, EXPECTED.stateContractSha256); + if ((generation === null) !== (contract === null)) { + throw new Error("expected incarnation state lineage is partial"); + } + + return { + target: text(expected, EXPECTED.target, "expected incarnation"), + planId: text(expected, EXPECTED.planId, "expected incarnation"), + incarnationId: text(expected, EXPECTED.incarnationId, "expected incarnation"), + sealedReleaseId: text(expected, EXPECTED.sealedReleaseId, "expected incarnation"), + runtimeId: text(expected, EXPECTED.runtimeId, "expected incarnation"), + runtimeInstanceId: text(activation, ACTIVATION.runtimeInstanceId, "runtime activation"), + expectedProjectionSha256: text( + activation, + ACTIVATION.expectedProjectionSha256, + "runtime activation", + ), + // The witness is the digest of the activation record itself, not a field + // inside it: it is what binds this statement to the exact launch Idunn + // recorded, so it is computed over the bytes actually delivered. + activationWitnessSha256: `sha256-${createHash("sha256").update(activationPayload).digest("hex")}`, + activationSignerIdentityId: text( + activation, + ACTIVATION.activationSignerIdentityId, + "runtime activation", + ), + stateSchemaGeneration: generation, + stateContractSha256: contract, + capabilities: capabilities(expected, EXPECTED.capabilities), + healthContract: text(expected, EXPECTED.healthContract, "expected incarnation"), + }; +} + +/** + * Open the activation signing key Idunn passed as a descriptor. + * + * The credential is exactly 32 bytes: the raw Ed25519 seed. It is passed as a + * parent-only descriptor and not as a path, so only the process Idunn launched + * can produce this half of the proof. + */ +export function openActivationSigner(env: NodeJS.ProcessEnv = process.env): KeyObject | undefined { + const descriptor = passedDescriptor(IDUNN_RUNTIME_ACTIVATION_CREDENTIAL_NAME, env); + if (descriptor === undefined) { + return undefined; + } + const seed = readFileSync(descriptor); + if (seed.length !== 32) { + throw new Error("Idunn runtime activation credential is not a 32-byte seed"); + } + return createPrivateKey({ + key: Buffer.concat([PKCS8_ED25519_SEED_PREFIX, seed]), + format: "der", + type: "pkcs8", + }); +} + +/** + * The publisher sequence must strictly increase for the life of a signer + * identity, because Idunn treats a repeat or a regression as a replay and + * refuses the observation. + * + * An in-process counter is therefore not enough: a restart would reset it and + * every statement afterwards would be refused until the count climbed past the + * old high-water mark. It lives beside the target's state instead, and is + * advanced before use. + */ +export async function nextPublisherSequence(sequencePath: string): Promise { + let current = 0; + try { + const raw = (await readFile(sequencePath, "utf8")).trim(); + const parsed = Number(raw); + if (Number.isSafeInteger(parsed) && parsed > 0) { + current = parsed; + } + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") { + throw error; + } + } + const next = current + 1; + // Written through a temporary file: a torn write here would look like a + // regression and cost the target every future observation. + const temporary = `${sequencePath}.next`; + await writeFile(temporary, `${next}\n`, { mode: 0o600 }); + await rename(temporary, sequencePath); + return next; +} + +export interface PresenceStatementInputs { + bundle: RuntimeBundleFacts; + state: RuntimePresenceState; + detail: string; + boundEndpoint: string | null; + writeLeaseSha256: string | null; + observedAtUnixMillis: number; + publisherSequence: number; + providerHealthIdentityPath: string; + env?: NodeJS.ProcessEnv; +} + +export interface PresenceStatement { + target: string; + payload: Uint8Array; +} + +/** + * Build and dual-sign one presence statement. + * + * Returns the target alongside the bytes because the document key must equal + * the signed target; Odin refuses the pair when they differ, and that is the + * one part of the envelope the transport cannot infer. + */ +export async function buildPresenceStatement( + inputs: PresenceStatementInputs, +): Promise { + const env = inputs.env ?? process.env; + const activationKey = openActivationSigner(env); + if (activationKey === undefined) { + throw new Error( + "Idunn passed no runtime activation credential, so this process cannot prove it is the " + + "incarnation Idunn started", + ); + } + const identity = await openProviderHealthIdentity(inputs.providerHealthIdentityPath, env); + + const presence: RuntimePresenceHealth = { + target: inputs.bundle.target, + expectedProjectionSha256: inputs.bundle.expectedProjectionSha256, + planId: inputs.bundle.planId, + incarnationId: inputs.bundle.incarnationId, + sealedReleaseId: inputs.bundle.sealedReleaseId, + activationWitnessSha256: inputs.bundle.activationWitnessSha256, + stateSchemaGeneration: inputs.bundle.stateSchemaGeneration, + stateContractSha256: inputs.bundle.stateContractSha256, + runtimeId: inputs.bundle.runtimeId, + runtimeInstanceId: inputs.bundle.runtimeInstanceId, + boundEndpoint: inputs.boundEndpoint, + capabilities: inputs.bundle.capabilities, + healthContract: inputs.bundle.healthContract, + state: inputs.state, + detail: inputs.detail, + writeLeaseSha256: inputs.writeLeaseSha256, + signerIdentityId: identity.identityId, + publisherSequence: inputs.publisherSequence, + observedAtUnixMillis: inputs.observedAtUnixMillis, + activationSignerIdentityId: inputs.bundle.activationSignerIdentityId, + }; + + const proof = runtimePresenceProofPayload(presence); + const signature = signWithIdentity(identity, runtimePresenceProviderSigningMessage(proof)); + const activationSignature = signWithIdentity( + { ...identity, privateKey: activationKey }, + runtimePresenceActivationSigningMessage(proof), + ); + + return { + target: presence.target, + payload: encodeRuntimePresenceHealth(presence, signature, activationSignature), + }; +} diff --git a/tests/idunn-rudp-health.test.ts b/tests/idunn-rudp-health.test.ts index 6bc1e09..66127d4 100644 --- a/tests/idunn-rudp-health.test.ts +++ b/tests/idunn-rudp-health.test.ts @@ -62,11 +62,21 @@ describe("Heimdall Idunn health publication", () => { }); const observedAt = "2026-08-22T00:45:00.000Z"; + // The transport is handed an already-signed statement; building one is + // the presence module's job and is covered against the Rust vectors in + // cultnet-ts. What this asserts is that the envelope keys the document by + // the signed target, which is the pairing Odin refuses when it differs. + const presence = { + target: "heimdall", + payload: new Uint8Array([0x01, 0x02, 0x03]), + }; + await publishIdunnRudpHealth(config, { daemonId: "yggdrasil-heimdall", state: "active", detail: "private-command-plane-ready", observedAt, + presence, }); const frame = await frameReceived; const message = parseCultNetMessage(decode(frame.payload), "cultnet.schema.v0"); @@ -76,17 +86,10 @@ describe("Heimdall Idunn health publication", () => { if (message.schemaVersion !== "cultnet.document_put_raw.v0") { throw new Error("Expected raw document publication."); } - expect(message.document.schemaId).toBe("idunn.signed_daemon_health.v1"); - expect(message.document.recordKey).toBe("yggdrasil-heimdall"); - expect(message.document.sourceRuntimeId).toBe("heimdall-service"); - expect(message.document.sourceRole).toBe("daemon-health-publisher"); - const statement = decode(message.document.payload) as unknown[]; - expect(statement).toHaveLength(17); - expect(statement[0]).toBe("idunn.signed_daemon_health.v1"); - expect(statement[1]).toBe("yggdrasil-heimdall"); - expect(statement[15]).toBeInstanceOf(Uint8Array); - expect((statement[15] as Uint8Array).byteLength).toBe(64); - expect(statement[16]).toBe(false); + expect(message.document.schemaId).toBe("gamecult.runtime_presence_health.v2"); + expect(message.document.recordKey).toBe(presence.target); + expect(message.document.sourceRole).toBe("runtime-presence-publisher"); + expect(Buffer.from(message.document.payload)).toEqual(Buffer.from(presence.payload)); } finally { server.close(); } diff --git a/tests/presence-identity-descriptor.test.ts b/tests/presence-identity-descriptor.test.ts index 8b2628b..d8ede1c 100644 --- a/tests/presence-identity-descriptor.test.ts +++ b/tests/presence-identity-descriptor.test.ts @@ -63,8 +63,7 @@ describe.skipIf(!machineIdAvailable)("presence identity from a passed descriptor const opened = await openProviderHealthIdentity( path.join(dir, "would-be-self-enrolled.cc"), - { LISTEN_FDNAMES: names, LISTEN_PID: String(process.pid) }, - process.pid + { LISTEN_FDNAMES: names, LISTEN_PID: String(process.pid) } ); // Same key means it came from the descriptor. A fresh self-enrolment @@ -82,24 +81,25 @@ describe.skipIf(!machineIdAvailable)("presence identity from a passed descriptor describe("presence identity descriptor selection", () => { const dir = os.tmpdir(); - it("ignores descriptors addressed to another process", async () => { - // LISTEN_PID naming a different process means these descriptors are not - // ours; consuming them would read a sibling's identity. The call must take - // the path branch, so it must not fail with a descriptor-read error. - await openProviderHealthIdentity( - path.join(dir, `heimdall-wrong-pid-${process.pid}.cc`), - { LISTEN_FDNAMES: "gamecult-runtime-presence-identity", LISTEN_PID: String(process.pid + 1) }, - process.pid - ).catch((error) => { - expect(String(error)).not.toContain("Idunn runtime presence identity"); - }); + it("takes the named descriptor even when LISTEN_PID is another namespace's pid", async () => { + // Idunn launches candidates with PrivatePIDs=yes, so systemd sets + // LISTEN_PID to the pid it knows in the outer namespace while this process + // sees a namespace-local one. They never match. Refusing the descriptor on + // that basis sent Heimdall down the self-enrolling path, where it signed + // health with a key nothing trusts and warmed forever -- so the descriptor + // must still be taken, and the attempt to read it is the proof. + await expect( + openProviderHealthIdentity(path.join(dir, `heimdall-outer-pid-${process.pid}.cc`), { + LISTEN_FDNAMES: "gamecult-runtime-presence-identity", + LISTEN_PID: String(process.pid + 1), + }) + ).rejects.toThrow(); }); it("ignores a descriptor set that does not name the presence identity", async () => { await openProviderHealthIdentity( path.join(dir, `heimdall-other-fd-${process.pid}.cc`), - { LISTEN_FDNAMES: "some-other-credential", LISTEN_PID: String(process.pid) }, - process.pid + { LISTEN_FDNAMES: "some-other-credential", LISTEN_PID: String(process.pid) } ).catch((error) => { expect(String(error)).not.toContain("Idunn runtime presence identity"); }); diff --git a/vendor/CultLib b/vendor/CultLib index 5cefa0d..d1a9eda 160000 --- a/vendor/CultLib +++ b/vendor/CultLib @@ -1 +1 @@ -Subproject commit 5cefa0db0079a8e3ee22f29d7b9e6e5aa60912a9 +Subproject commit d1a9edad3a116be341b0b5198c6998c31a083bda