Files
MikrotikManager/backend/src/services/bandwidth-external-node-registry.test.ts
T

114 lines
13 KiB
TypeScript

import assert from "node:assert/strict"
import { test } from "node:test"
import { createHmac, randomUUID } from "node:crypto"
import { createBandwidthExternalNodeRegistry, BandwidthExternalNodeRegistryError, BandwidthExternalNodeRegistryUnknown } from "./bandwidth-external-node-registry.js"
import { createBandwidthNodeRegistry } from "./bandwidth-node-registry.js"
import type { BandwidthPeerObservationEvidence } from "./bandwidth-peer-observation.js"
import type { ProbeStorageConnection } from "./server-probe-journal-maintenance.js"
type Row=Record<string,unknown>
const canonical=(v:unknown):string=>Array.isArray(v)?"["+v.map(canonical).join(",")+"]":v&&typeof v==="object"?"{"+Object.entries(v).sort(([a],[b])=>a.localeCompare(b)).map(([k,v])=>JSON.stringify(k)+":"+canonical(v)).join(",")+"}":JSON.stringify(v)
const mac=(purpose:string,value:unknown)=>createHmac("sha256","synthetic-fixed-issuer-only").update(purpose+":"+canonical(value)).digest("hex")
const installationId=randomUUID(),issuerKeySha256="a".repeat(64),quiescenceProof="b".repeat(64)
const root={installationId,issuerKeySha256,quiescenceProof}
const scope=()=>({deadlineAt:Date.now()+5000})
/** Inert SQL state machine, no pool/PG/remote imports. The same node table and
* MAC issuer are shared by the real managed and external registry functions. */
function fixture(){
const nodes:Row[]=[],aliases:Row[]=[],events:Row[]=[],managedAliases:Row[]=[],managedEvents:Row[]=[],queries:string[]=[],purposes:string[]=[]
const occupancy={status:"unknown",node_id:"never-changed"},observations=new WeakMap<object,BandwidthPeerObservationEvidence>()
let revoked=false,keyChanged=false,commitMode:"ok"|"lost"|"rows"="ok",afterEvent=false,finalSelect=false,inspectionsAfterSelect=0, revokeAtCommit=false,leaseCount=0
let onProof:((purpose:string)=>void)|undefined
const authority=()=>{if(revoked)throw Error("original grant revoked");return{...root,issuerKeySha256:keyChanged?"f".repeat(64):issuerKeySha256,authorityProof:"c".repeat(64),authorityGeneration:"original-grant",expiresAt:Date.now()+60000}}
// Stable original expiry: extending it at every check must be refused.
const expiresAt=Date.now()+60000
const authorize=async()=>({...authority(),expiresAt})
const inspect=async(token:unknown)=>{
const e=token&&typeof token==="object"?observations.get(token):undefined;if(!e)throw Error("not genuine")
if(revokeAtCommit&&finalSelect&&++inspectionsAfterSelect===3)revoked=true
return e
}
const connect=async():Promise<ProbeStorageConnection>=>{
leaseCount++;const before=structuredClone({nodes,aliases,events,managedAliases,managedEvents});let committed=false
const restore=()=>{for(const [target,old]of[[nodes,before.nodes],[aliases,before.aliases],[events,before.events],[managedAliases,before.managedAliases],[managedEvents,before.managedEvents]])target.splice(0,target.length,...old)}
return{release(){if(!committed)restore()},async query(sql,values=[]){
queries.push(sql);const rows=(r:Row[]=[],command?:string)=>({rows:structuredClone(r),command})
if(sql==="BEGIN")return rows([],"BEGIN")
if(sql==="ROLLBACK"){restore();return rows([],"ROLLBACK")}
if(sql==="COMMIT"){committed=true;if(commitMode==="lost")throw Error("lost physical ACK");return rows([],commitMode==="rows"?undefined:"COMMIT")}
if(sql.includes("set_config")||sql.includes("pg_advisory_xact_lock"))return rows()
if(sql.includes("FROM mmapp_control.bandwidth_node_authority")){if(afterEvent)finalSelect=true;return rows([{installation_id:installationId,issuer_key_sha256:issuerKeySha256,quiescence_proof:quiescenceProof,status:"active"}])}
const isExternal=sql.includes("bandwidth_external_node_"),a=isExternal?aliases:managedAliases,e=isExternal?events:managedEvents,id=isExternal?"enrollment_id":"server_id"
if(sql.startsWith("WITH aliases AS MATERIALIZED")){
const selected=a.filter(row=>row[id]===values[0])
return rows([{aliases:selected.map(row=>({subject:String(row[id]),incarnation:row.incarnation,node_id:row.node_id,status:row.status,record_mac:row.record_mac})),nodes:nodes.filter(row=>selected.some(alias=>alias.node_id===row.node_id)),events:e.filter(row=>row[id]===values[0]).map(row=>({event_id:row.event_id,incarnation:row.incarnation,node_id:row.node_id,kind:row.kind,old_status:row.old_status,new_status:row.new_status,authority_proof:row.authority_proof,event_mac:row.event_mac})),claim_nodes:values[1]===null?[]:nodes.filter(row=>row.claim_proof===values[1])}],"SELECT")
}
if(sql.startsWith("DO $mmapp_bandwidth_register$")){
// Inert adapter parses only the candidate's validated literal VALUES;
// this model does not certify PostgreSQL sequential SPI semantics.
const valuesOf=(table:string)=>{const line=sql.split("\n").find(line=>line.trimStart().startsWith("INSERT INTO mmapp_control."+table+"("));assert(line);const match=/ VALUES\((.*)\)$/.exec(line!);assert(match);return match![1].split(",").map(value=>value.replace(/::pg_catalog\.(?:uuid|text|int8)$/,"" ).replace(/^'|'$/g,""))}
const node=valuesOf("bandwidth_nodes")
if(sql.includes("IF true THEN")){assert(!nodes.some(row=>row.claim_proof===node[1]));nodes.push({node_id:node[0],claim_proof:node[1],enrollment_mac:node[2]})}
else assert(nodes.some(row=>row.node_id===node[0]&&row.claim_proof===node[1]&&row.enrollment_mac===node[2]))
const alias=valuesOf(isExternal?"bandwidth_external_node_aliases":"bandwidth_node_aliases"),event=valuesOf(isExternal?"bandwidth_external_node_events":"bandwidth_node_events"),subject=isExternal?alias[0]:Number(alias[0])
assert(!a.some(row=>row[id]===subject));a.push({[id]:subject,incarnation:alias[1],node_id:alias[2],status:alias[3],record_mac:alias[4]})
e.push({event_id:event[0],[id]:isExternal?event[1]:Number(event[1]),incarnation:event[2],node_id:event[3],kind:event[4],old_status:event[5]==="NULL"?null:event[5],new_status:event[6],authority_proof:event[7],event_mac:event[8]});afterEvent=true
return rows([],"DO")
}
if(sql.startsWith("SELECT")&&sql.includes("FROM mmapp_control.bandwidth_nodes"))return rows(nodes.filter(row=>row[sql.includes("WHERE claim_proof")?"claim_proof":"node_id"]===values[0]))
if(sql.startsWith("INSERT INTO mmapp_control.bandwidth_nodes")){const row={node_id:values[0],claim_proof:values[1],enrollment_mac:values[2]};assert(!nodes.some(n=>n.claim_proof===row.claim_proof));nodes.push(row);return rows([{node_id:row.node_id}])}
if(sql.startsWith("SELECT")&&sql.includes("_aliases WHERE"))return rows(a.filter(row=>row[id]===values[0]))
if(sql.startsWith("SELECT")&&sql.includes("_events WHERE"))return rows(e.filter(row=>row[id]===values[0]).sort((a,b)=>String(a.kind).localeCompare(String(b.kind))))
if(sql.startsWith("INSERT")&&sql.includes("_aliases(")){const row={[id]:values[0],incarnation:values[1],node_id:values[2],status:"active",record_mac:values[3]};assert(!a.some(r=>r[id]===row[id]));a.push(row);return rows([{[id]:row[id]}])}
if(sql.startsWith("INSERT")&&sql.includes("_events(")){e.push({event_id:values[0],[id]:values[1],incarnation:values[2],node_id:values[3],kind:values[4],old_status:values[5],new_status:values[6],authority_proof:values[7],event_mac:values[8]});afterEvent=true;return rows([{event_id:values[0]}])}
if(sql.startsWith("UPDATE")&&sql.includes("_aliases SET")){const row=a.find(r=>r[id]===values[0]&&r.status==="active"&&r.record_mac===values[3]);if(!row)return rows();row.status=values[1];row.record_mac=values[2];return rows([{[id]:row[id]}])}
throw Error("Unexpected SQL: "+sql)
}}
}
const keyedProof=async(purpose:string,value:unknown)=>{purposes.push(purpose);onProof?.(purpose);return mac(purpose,value)}
const host=createBandwidthExternalNodeRegistry({now:Date.now,connect,inspect,authorize,keyedProof})
function observation(enrollmentId:string=randomUUID(),claims:BandwidthPeerObservationEvidence["claims"]=[{kind:"system-id",value:"shared-identity"}]){
const token={};observations.set(token,{version:2,peerRef:{kind:"external",enrollmentId},claims,sourceProof:"d".repeat(64),authorityGeneration:"source-v2",routingEndpointEpoch:randomUUID(),observedAt:Date.now(),expiresAt:Date.now()+5000,physicalUniquenessVerified:false,cloneResistanceVerified:false});return token
}
const managed=createBandwidthNodeRegistry({now:Date.now,connect,authorize,keyedProof,inspect:async()=>({version:1,serverId:42,claims:[{kind:"system-id",value:"shared-identity"}],sourceProof:"d".repeat(64),authorityGeneration:"source-v1",routingEndpointEpoch:installationId,observedAt:1,expiresAt,physicalUniquenessVerified:false,cloneResistanceVerified:false})})
return{host,managed,observation,nodes,aliases,events,queries,purposes,occupancy,setCommit:(mode:typeof commitMode)=>{commitMode=mode},revoke:()=>{revoked=true},changeKey:()=>{keyChanged=true},revokeAtCommit:()=>{revokeAtCommit=true;afterEvent=false;finalSelect=false;inspectionsAfterSelect=0},proofHook:(hook:typeof onProof)=>{onProof=hook},leases:()=>leaseCount}
}
test("managed and external aliases use the identical issuer/typed claim domain and canonical node; unknown occupancy untouched",async()=>{
const f=fixture(),before=structuredClone(f.occupancy),managed=await f.managed.register({},scope()),token=f.observation(),external=await f.host.register(token,scope())
assert.equal(external.canonicalNodeId,managed.canonicalNodeId);assert.notEqual(external.incarnation,managed.incarnation);assert.equal(f.nodes.length,1)
assert.equal((await f.host.resolve(token,scope())).canonicalNodeId,managed.canonicalNodeId)
assert.equal((await f.host.register(token,scope())).outcome,"existing");assert.equal(f.events.length,1)
assert(f.purposes.includes("external-alias"));assert(f.purposes.includes("external-event"));assert.deepEqual(f.occupancy,before)
assert(f.queries.includes("SELECT pg_catalog.pg_advisory_xact_lock(8723116)"));assert(!f.queries.some(q=>/occupancy|bandwidth_attempt/.test(q)))
})
test("external terminal alias cannot reactivate/rebind; fresh alias preserves node and unknown",async()=>{
const f=fixture(),token=f.observation(),first=await f.host.register(token,scope())
assert.equal((await f.host.retire(first.enrollmentId,"tombstone",scope())).status,"tombstoned")
assert.equal((await f.host.retire(first.enrollmentId,"tombstone",scope())).outcome,"existing")
await assert.rejects(f.host.register(token,scope()));await assert.rejects(f.host.resolve(token,scope()));await assert.rejects(f.host.retire(first.enrollmentId,"quarantine",scope()))
const replacement=await f.host.register(f.observation(),scope());assert.equal(replacement.canonicalNodeId,first.canonicalNodeId);assert.notEqual(replacement.incarnation,first.incarnation);assert.equal(f.events.length,3)
})
test("resolve absent, cloned observation, invalid enrollment and mismatched license do not auto-enroll",async()=>{
const f=fixture(),token=f.observation();await assert.rejects(f.host.resolve(token,scope()));assert.equal(f.nodes.length,0)
await assert.rejects(f.host.register({...token},scope()));await assert.rejects(f.host.retire("42","tombstone",scope()));assert.equal(f.nodes.length,0)
const first=await f.host.register(token,scope());await assert.rejects(f.host.register(f.observation(first.enrollmentId,[{kind:"software-id",value:"shared-identity"}]),scope()));assert.equal(f.nodes.length,1)
})
test("full typed license vector and claim kind remain distinct",async()=>{
const f=fixture(),ids=[]
for(const claims of [[{kind:"system-id",value:"ONE"}],[{kind:"software-id",value:"ONE"}],[{kind:"system-id",value:"ONE"},{kind:"software-id",value:"TWO"}]] as BandwidthPeerObservationEvidence["claims"][])ids.push((await f.host.register(f.observation(undefined,claims),scope())).canonicalNodeId)
assert.equal(new Set(ids).size,3)
})
test("revocation or issuer drift during proof prevents writes, destroys lease and rolls back",async()=>{
for(const change of ["revoke","changeKey"] as const){const f=fixture();f.proofHook(purpose=>{if(purpose==="external-event")f[change]()});await assert.rejects(f.host.register(f.observation(),scope()),BandwidthExternalNodeRegistryError);assert.equal(f.nodes.length,0);assert.equal(f.aliases.length,0);assert(!f.queries.includes("COMMIT"));assert(f.queries.includes("ROLLBACK"))}
})
test("original grant is checked after final asynchronous inspection before COMMIT",async()=>{
const f=fixture();f.revokeAtCommit();await assert.rejects(f.host.register(f.observation(),scope()),BandwidthExternalNodeRegistryError);assert(!f.queries.includes("COMMIT"));assert.equal(f.aliases.length,0)
})
test("lost or rows-only physical COMMIT ACK is unknown, no replay or post-COMMIT rollback; fresh resolve reconciles",async()=>{
for(const mode of ["lost","rows"] as const){const f=fixture(),token=f.observation();f.setCommit(mode);await assert.rejects(f.host.register(token,scope()),error=>error instanceof BandwidthExternalNodeRegistryUnknown&&error.automaticReplay===false);assert.equal(f.aliases.length,1);assert.equal(f.events.length,1);assert.equal(f.queries.filter(q=>q==="COMMIT").length,1);assert(!f.queries.includes("ROLLBACK"));f.setCommit("ok");assert.equal((await f.host.resolve(token,scope())).canonicalNodeId,f.nodes[0].node_id);assert.equal(f.events.length,1)}
})
test("prior alias/node/event MAC tampering refuses without replacement identity",async()=>{
for(const target of ["aliases","nodes","events"] as const){const f=fixture(),token=f.observation();await f.host.register(token,scope());const rows=f[target];rows[0][target==="aliases"?"record_mac":target==="nodes"?"enrollment_mac":"event_mac"]="f".repeat(64);await assert.rejects(f.host.resolve(token,scope()));assert.equal(f.nodes.length,1);assert.equal(f.events.length,1)}
})