diff --git a/tests/persona-feedback-idunn-health.test.mjs b/tests/persona-feedback-idunn-health.test.mjs index b335418..d816654 100644 --- a/tests/persona-feedback-idunn-health.test.mjs +++ b/tests/persona-feedback-idunn-health.test.mjs @@ -12,18 +12,18 @@ const root=resolve(import.meta.dirname,".."),cult=resolve(root,"..","CultLib"),r const idDomain=Buffer.from("gamecult.provider-health.identity.v1\0"),signatureDomain=Buffer.from("gamecult.provider-health.signature.v1\0"),purpose=Buffer.from("idunn.signed_daemon_health.v1"); test("Persona-feedback emits the exact canonical signed daemon-health tuple",async()=>{ - const identity=await identityFixture(),incarnation="019f523f-f650-7001-be75-b4428985652b",signed=createSignedPersonaFeedbackHealth({daemonId:"yggdrasil-bifrost-persona-feedback",healthContract:"bifrost.cultnet-rudp-persona-feedback-health",state:"active",detail:"ready",identity,publisherIncarnationId:incarnation,publisherSequence:7,observedAtUnixMillis:1773950400123}),record=decode(signed.payload); - assert.equal(record.length,17);assert.deepEqual(record.slice(0,10),["idunn.signed_daemon_health.v1","yggdrasil-bifrost-persona-feedback","bifrost.cultnet-rudp-persona-feedback-health","bifrost-persona-feedback","active","ready",signed.providerIdentityId,incarnation,7,1773950400123]);assert.deepEqual(record.slice(10,14),[null,null,null,null]);assert.equal(record[14],"ed25519");assert.ok(Array.isArray(record[15]));assert.equal(record[15].length,64);assert.equal(record[16],false); - const canonical=[...record];canonical[15]=[];assert.equal(verifyProvider(identity,encode(canonical),record[15]),true); + const identity=await identityFixture(),incarnation="019f523f-f650-7001-be75-b4428985652b",signed=createSignedPersonaFeedbackHealth({daemonId:"yggdrasil-bifrost-persona-feedback",healthContract:"bifrost.cultnet-rudp-persona-feedback-health",state:"active",detail:"ready",identity,publisherIncarnationId:incarnation,publisherSequence:7,observedAtUnixMillis:1773950400123}),record=decode(signed.payload,{useBigInt64:true}); + assert.equal(record.length,17);assert.deepEqual(record.slice(0,10),["idunn.signed_daemon_health.v1","yggdrasil-bifrost-persona-feedback","bifrost.cultnet-rudp-persona-feedback-health","bifrost-persona-feedback","active","ready",signed.providerIdentityId,incarnation,7,1773950400123n]);assert.deepEqual(record.slice(10,14),[null,null,null,null]);assert.equal(record[14],"ed25519");assert.ok(Array.isArray(record[15]));assert.equal(record[15].length,64);assert.equal(record[16],false); + const canonical=[...record];canonical[15]=[];assert.equal(verifyProvider(identity,encode(canonical,{useBigInt64:true}),record[15]),true); assert.equal(signed.providerIdentityId,createHash("sha256").update(idDomain).update(identity.publicKey).digest("hex"));assert.notEqual(signed.providerIdentityId,identity.identityId); }); test("provider-health signature rejects purpose, key, and payload substitution",async()=>{ - const identity=await identityFixture(),other=await identityFixture(),signed=createSignedPersonaFeedbackHealth({daemonId:"daemon",healthContract:"contract",state:"degraded",detail:"readiness-degraded",identity,publisherIncarnationId:"019f523f-f650-7001-be75-b4428985652b",publisherSequence:1,observedAtUnixMillis:1773950400123}),record=decode(signed.payload),canonical=[...record];canonical[15]=[]; - assert.equal(verifyProvider(identity,encode(canonical),record[15]),true);assert.equal(verifyProvider(other,encode(canonical),record[15]),false); - const mutated=[...canonical];mutated[4]="active";assert.equal(verifyProvider(identity,encode(mutated),record[15]),false); - const oldDomainSignature=signFeedbackAdmission(identity,encode(canonical));assert.equal(verifyProvider(identity,encode(canonical),oldDomainSignature),false); - const unsignedDiagnostic=[record[1],record[4],record[5],new Date(record[9]).toISOString(),record[2],"diagnostic-only","cultnet.transport.rudp.v0"];assert.equal(verifyProvider(identity,encode(unsignedDiagnostic),record[15]),false); + const identity=await identityFixture(),other=await identityFixture(),signed=createSignedPersonaFeedbackHealth({daemonId:"daemon",healthContract:"contract",state:"degraded",detail:"readiness-degraded",identity,publisherIncarnationId:"019f523f-f650-7001-be75-b4428985652b",publisherSequence:1,observedAtUnixMillis:1773950400123}),record=decode(signed.payload,{useBigInt64:true}),canonical=[...record];canonical[15]=[]; + const canonicalPayload=encode(canonical,{useBigInt64:true});assert.equal(verifyProvider(identity,canonicalPayload,record[15]),true);assert.equal(verifyProvider(other,canonicalPayload,record[15]),false); + const mutated=[...canonical];mutated[4]="active";assert.equal(verifyProvider(identity,encode(mutated,{useBigInt64:true}),record[15]),false); + const oldDomainSignature=signFeedbackAdmission(identity,canonicalPayload);assert.equal(verifyProvider(identity,canonicalPayload,oldDomainSignature),false); + const unsignedDiagnostic=[record[1],record[4],record[5],new Date(Number(record[9])).toISOString(),record[2],"diagnostic-only","cultnet.transport.rudp.v0"];assert.equal(verifyProvider(identity,encode(unsignedDiagnostic),record[15]),false); }); test("publisher sequence and process incarnation are mandatory protocol state",async()=>{ diff --git a/tools/persona-feedback-idunn-health.mjs b/tools/persona-feedback-idunn-health.mjs index 90328c7..7986b22 100644 --- a/tools/persona-feedback-idunn-health.mjs +++ b/tools/persona-feedback-idunn-health.mjs @@ -28,11 +28,15 @@ export function createSignedPersonaFeedbackHealth(input){ // Rust's canonical positional contract represents Vec as a MessagePack // integer array. Uint8Array lowers to bin8 and is therefore a different // signed payload even though both decode to bytes in JavaScript. - const provider=providerHealthIdentity(input.identity),record=[schema,input.daemonId,input.healthContract,sourceRuntimeId,input.state,input.detail,provider.identityId,input.publisherIncarnationId,input.publisherSequence,observed,null,null,null,null,"ed25519",[],false]; - const proof=signProviderHealth(input.identity,encode(record));if(proof.identityId!==provider.identityId)throw new Error("Provider-health signer identity derivation disagrees with the packet.");record[15]=Array.from(proof.signature); - return {record,payload:encode(record),providerIdentityId:provider.identityId,observedAtUnixMillis:observed}; + // Safe JavaScript numbers above u32 lower as float64. Rust's u64 lowers as + // uint64, so retain the timestamp as BigInt on this signed wire surface. + const provider=providerHealthIdentity(input.identity),record=[schema,input.daemonId,input.healthContract,sourceRuntimeId,input.state,input.detail,provider.identityId,input.publisherIncarnationId,input.publisherSequence,BigInt(observed),null,null,null,null,"ed25519",[],false]; + const proof=signProviderHealth(input.identity,canonicalHealthEncode(record));if(proof.identityId!==provider.identityId)throw new Error("Provider-health signer identity derivation disagrees with the packet.");record[15]=Array.from(proof.signature); + return {record,payload:canonicalHealthEncode(record),providerIdentityId:provider.identityId,observedAtUnixMillis:observed}; } +function canonicalHealthEncode(value){return encode(value,{useBigInt64:true});} + export async function publishPersonaFeedbackHealth(input){ if(!input.endpoint)throw new Error("Bifrost Persona-feedback Idunn health endpoint is required."); if(!input.publisher||typeof input.publisher.next!=="function")throw new Error("Bifrost Persona-feedback process-scoped health publisher is required.");