Files
MikrotikManager/backend/src/modules/mcp/routing-manual-inverse-history.test.ts
T

214 lines
19 KiB
TypeScript

import assert from "node:assert/strict"
import { randomUUID } from "node:crypto"
import { mkdtemp, rm } from "node:fs/promises"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { test } from "node:test"
import type { PoolClient } from "pg"
import { routeAiProfileSchema } from "@mmapp/contracts/mcp-routing"
import { decryptSecretExisting, encryptSecret, fingerprint, privateSecretFingerprint, redact } from "./security.js"
import { assertLegacyManualHistoryWithinOwner } from "./routing-manual-inverse-history.js"
import { manualInverseReceiptSchema, retainManualInverseChargesWithinOwner } from "./routing-manual-inverse-store.js"
import { scopedRecoveryIntentFromCheckpoint, assertPrivateRecoverySize } from "./routing-community-scoped-recovery.js"
const at="2026-11-01T12:00:00.000Z", sourceAt="2026-11-01T11:59:00.000Z", until="2026-11-01T12:01:00.000Z"
const sign=(value:unknown,domain:string)=>privateSecretFingerprint(fingerprint(value),domain)
async function fixture(persistent=false,version=6) {
const connectionId=randomUUID(),credentialId=randomUUID(),profileId=randomUUID(),sourceId=randomUUID(),operationId=randomUUID(),ownerId="fixture-owner"
const principal={connectionId,credentialId,credentialExpiresAt:until}
const profile=routeAiProfileSchema.parse({name:"manual fixture",serverId:1,community:"65000:10",family:"ipv4",autoRecovery:false,
candidates:[{id:"a",jumpHostId:2,exitNodeId:3,wanInterface:"wan",downstreamInterface:"hop",gateway:"192.0.2.1",
gatewayRef:{resource:"ipv4_route",id:"*1"},controlProbe:{targetInterface:"exit",expectedHops:[{serverId:3,interface:"independent-hop"}]}}]})
const input:Record<string,unknown>={proofRevision:persistent?3:2,receiptId:randomUUID(),ownerId,principal,profile}
if(persistent) {
const packet={revision:1,profileId,profileVersion:1,connectionId,credentialId,ownerId,principalExpiresAt:until,credentialExpiresAt:until,
grantExpiresAt:until,ownerExpiresAt:until,policyExpiresAt:until,validUntil:until,generation:"fixture-generation",connectionGeneration:"fixture-generation",
policyVersion:1,policyProof:"a".repeat(64),ownerPermissionsProof:"b".repeat(64),profileProof:fingerprint(profile),scope:[1,2,3],transportProof:"c".repeat(64)}
input.persistentProfileAuthority={packet,proof:await sign(packet,"route-ai-persistent-profile-authority-v1")}
}
input.requestProof=await sign(input,`route-ai-scoped-request-v${input.proofRevision}`)
const step={kind:"service",service:"routing.community.scoped",input}, config={revision:persistent?2:1,phase:"CONFIG_APPLIED",operationId:sourceId,stepIndex:0,
connectionId,ownerId,receiptId:input.receiptId,requestSeal:input.requestProof,profileProof:fingerprint(profile),observedAt:sourceAt,
...(persistent?{persistentProfileAuthority:input.persistentProfileAuthority}:{})}
const configurationProof=await sign(config,`route-ai-scoped-config-checkpoint-v${config.revision}`)
const generic={state:"applied",intended_state:redact(step),result:{configurationCheckpointProof:configurationProof,networkCheckpointProof:"d".repeat(64)}}
const sourceProof=await sign({operationId:sourceId,state:"partial",createdAt:sourceAt,sourceAction:"routing.community.switch",sourceVersion:7,
inputSeal:input.requestProof,configurationProof,networkSeal:generic.result.networkCheckpointProof,generic},"route-ai-scoped-recovery-source-v1")
const request:Record<string,unknown>={proofRevision:1,mode:"manual",sourceOperationId:sourceId,sourceReceiptId:input.receiptId,sourceProof,configurationProof,
receiptId:randomUUID(),ownerId,principal,profile,validUntil:until}
request.requestProof=await sign(request,"route-ai-scoped-recovery-request-v1")
const intent={revision:1,phase:"RECOVERY_INTENT",mode:"manual",sourceOperationId:sourceId,sourceStepIndex:0,sourceReceiptId:input.receiptId,sourceProof,configurationProof,
operationId,stepIndex:0,receiptId:request.receiptId,requestSeal:request.requestProof,connectionId,ownerId,validUntil:until,createdAt:at}
const row={source_operation_id:sourceId,source_step_index:0,source_receipt_id:input.receiptId,source_configuration_proof:configurationProof,recovery_receipt_id:request.receiptId,
recovery_operation_id:operationId,intent_sealed:await encryptSecret(JSON.stringify(intent)),intent_proof:await sign(intent,"route-ai-scoped-recovery-intent-v1"),
deadline:new Date(until),reservation_id:null,source_plan_id:randomUUID(),source_connection_id:connectionId,source_state:"partial",source_created_at:new Date(sourceAt),
source_action:"routing.community.switch",source_version:7,source_evidence:await encryptSecret(JSON.stringify({steps:[step]})),recovery_plan_id:randomUUID(),
recovery_connection_id:connectionId,recovery_action:"operations.recover",recovery_version:version,
recovery_evidence:await encryptSecret(JSON.stringify({steps:[{kind:"service",service:"routing.community.scoped.recover",input:request}],validUntil:until})),
configuration_sealed:await encryptSecret(JSON.stringify(config)),configuration_proof:configurationProof,generic_state:generic.state,generic_intended:generic.intended_state,generic_result:generic.result}
return {row,profileId,input,intent}
}
function cursor(rows:unknown[],onFetch?:()=>void) {
let offset=0;const calls:string[]=[]
const db={query:async(sql:string)=>{calls.push(sql);if(sql.startsWith("DECLARE"))return{rows:[]};if(sql.startsWith("FETCH")){onFetch?.();const page=rows.slice(offset,offset+12);offset+=12;return{rows:page}}if(sql.startsWith("CLOSE"))return{rows:[]};if(sql.includes("FROM mcp_routing_manual_inverse_charges"))return{rows:[]};throw Error(`Unexpected SQL ${sql}`)}} as unknown as PoolClient
return {db,calls}
}
test("historical cursor authenticates more than two pages of genuine nonpersistent classifications",async()=>{
const dir=await mkdtemp(join(tmpdir(),"mm-manual-history-unit-")),previous=process.env.MCP_STATE_DIR;process.env.MCP_STATE_DIR=dir
try {
const rows=[];for(let n=0;n<25;n++)rows.push((await fixture(false,6+n%3)).row)
const f=cursor(rows)
await assertLegacyManualHistoryWithinOwner(f.db,randomUUID(),at,()=>{})
assert.equal(f.calls.filter(sql=>sql.startsWith("FETCH")).length,3)
assert.equal(f.calls.at(-1),"CLOSE mcp_manual_history")
assert.match(f.calls[0],/^DECLARE .*NO SCROLL CURSOR WITHOUT HOLD FOR SELECT/)
assert.doesNotMatch(f.calls[0],/created_at\s*[<>]|LIMIT\s+12|schema_migrations/)
}finally{if(previous===undefined)delete process.env.MCP_STATE_DIR;else process.env.MCP_STATE_DIR=previous;await rm(dir,{recursive:true,force:true})}
})
test("historical authentication, active persistent refusal, future rejection and timeout never yield partial success",async()=>{
const dir=await mkdtemp(join(tmpdir(),"mm-manual-history-negative-")),previous=process.env.MCP_STATE_DIR;process.env.MCP_STATE_DIR=dir
try {
const f=await fixture(true)
await assert.rejects(assertLegacyManualHistoryWithinOwner(cursor([f.row]).db,f.profileId,at,()=>{}),/hour incomplete/)
await assertLegacyManualHistoryWithinOwner(cursor([f.row]).db,randomUUID(),at,()=>{})
await assertLegacyManualHistoryWithinOwner(cursor([f.row]).db,f.profileId,"2026-11-01T13:01:00.001Z",()=>{})
const expiredTamper={...f.row,source_evidence:null}
await assert.rejects(assertLegacyManualHistoryWithinOwner(cursor([expiredTamper]).db,f.profileId,"2026-11-01T14:00:00.000Z",()=>{}))
for (const version of [7,8]) {
const current={...f.row,recovery_version:version}
await assert.rejects(assertLegacyManualHistoryWithinOwner(cursor([current]).db,f.profileId,"2026-11-01T14:00:00.000Z",()=>{}),/lacks durable charge/)
}
const stripped={...f.input};delete stripped.persistentProfileAuthority
await assert.rejects(assertLegacyManualHistoryWithinOwner(cursor([{...f.row,source_evidence:await encryptSecret(JSON.stringify({steps:[{kind:"service",service:"routing.community.scoped",input:stripped}]}))}]).db,f.profileId,at,()=>{}))
const future={...f.intent,createdAt:"2026-11-01T12:00:00.001Z"}
await assert.rejects(assertLegacyManualHistoryWithinOwner(cursor([{...f.row,intent_sealed:await encryptSecret(JSON.stringify(future)),intent_proof:await sign(future,"route-ai-scoped-recovery-intent-v1")}]).db,f.profileId,at,()=>{}))
const valid=await fixture(false);let expired=false;const c=cursor(Array(25).fill(valid.row),()=>{expired=true})
await assert.rejects(assertLegacyManualHistoryWithinOwner(c.db,randomUUID(),at,()=>{if(expired)throw Error("original deadline expired")}),/original deadline/)
assert.equal(c.calls.at(-1),"CLOSE mcp_manual_history")
}finally{if(previous===undefined)delete process.env.MCP_STATE_DIR;else process.env.MCP_STATE_DIR=previous;await rm(dir,{recursive:true,force:true})}
})
test("state transfer cap MODEL refuses oversized or NULL history as a whole, with no truncation",async()=>{
const dir=await mkdtemp(join(tmpdir(),"mm-manual-history-caps-unit-")),previous=process.env.MCP_STATE_DIR;process.env.MCP_STATE_DIR=dir
try {
// Models fixed PostgreSQL octet_length CASE only; no SQL is executed here.
const sqlStateCap=(raw:string|null)=>raw!==null&&Buffer.byteLength(raw,"utf8")<=32?raw:null
for(const value of ["partial","applied","verified"])assert.equal(sqlStateCap(value),value)
assert.equal(sqlStateCap("p".repeat(33)),null)
assert.equal(sqlStateCap("я".repeat(17)),null)
assert.equal(sqlStateCap("я".repeat(16)),"я".repeat(16))
const valid=await fixture(false,7)
for(const field of ["source_state","generic_state"] as const){
const oversized={...valid.row,[field]:sqlStateCap("partial"+"x".repeat(26))}
assert.equal(oversized[field],null)
const c=cursor([valid.row,oversized])
await assert.rejects(assertLegacyManualHistoryWithinOwner(c.db,randomUUID(),at,()=>{}))
assert.equal(c.calls.at(-1),"CLOSE mcp_manual_history")
assert(c.calls[0].includes("CASE WHEN octet_length(o.state)<=32 THEN o.state END AS source_state"))
assert(c.calls[0].includes("CASE WHEN octet_length(s.state)<=32 THEN s.state END AS generic_state"))
}
// A short but noncanonical value is not made legitimate by the byte cap.
await assert.rejects(assertLegacyManualHistoryWithinOwner(cursor([{...valid.row,source_state:sqlStateCap("partial ")}]).db,randomUUID(),at,()=>{}))
}finally{if(previous===undefined)delete process.env.MCP_STATE_DIR;else process.env.MCP_STATE_DIR=previous;await rm(dir,{recursive:true,force:true})}
})
async function retentionFixture(version = 7) {
const f=await fixture(true,version), authority=f.input.persistentProfileAuthority as {packet:{profileVersion:number;scope:number[]};proof:string}
const payload=await encryptSecret(JSON.stringify({MODEL:true}))
const row={...f.row,recovery_payload:payload,authority_profile_version:1}
const binding=manualInverseReceiptSchema.shape.binding.parse({revision:1,recoveryOperationId:f.intent.operationId,recoveryPlanId:row.recovery_plan_id,
sourceOperationId:f.intent.sourceOperationId,sourcePlanId:row.source_plan_id,sourceStepIndex:0,profileId:f.profileId,profileVersion:1,
connectionId:f.intent.connectionId,ownerId:f.intent.ownerId,originalPrincipal:f.input.principal,recoveryPrincipal:f.input.principal,
operationVersion:version,sourceVersion:7,authorityProof:authority.proof,sourceProof:f.intent.sourceProof,sourceRequestProof:f.input.requestProof,
recoveryRequestProof:f.intent.requestSeal,sourceEvidenceProof:fingerprint(row.source_evidence),recoveryEvidenceProof:fingerprint(row.recovery_evidence),
recoveryPayloadProof:fingerprint(payload),scope:authority.packet.scope,createdAt:at,chargeUnits:1})
const receipt={binding,proof:await sign(binding,"routing-manual-inverse-charge-v1")}
const intent={...f.intent,revision:2,manualInverseCharge:receipt}
row.intent_sealed=await encryptSecret(JSON.stringify(intent));row.intent_proof=await sign(intent,"route-ai-scoped-recovery-intent-v1")
const parent={recovery_operation_id:intent.operationId,recovery_plan_id:row.recovery_plan_id,source_operation_id:intent.sourceOperationId,source_plan_id:row.source_plan_id,
source_step_index:0,profile_id:f.profileId,profile_version:1,connection_id:intent.connectionId,owner_id:intent.ownerId,charge_units:1,
binding_sealed:await encryptSecret(JSON.stringify(binding)),binding_proof:receipt.proof,created_at:new Date(at)}
return {...f,row,intent,parent}
}
function retentionOwner(f:Awaited<ReturnType<typeof retentionFixture>>,present=true) {
const calls:string[]=[], deleted:string[]=[];let parent=present
const db={query:async(sql:string)=>{
calls.push(sql)
if(sql==='SHOW transaction_isolation')return{rows:[{transaction_isolation:'read committed'}]}
if(sql.includes('pg_advisory_xact_lock'))return{rows:[]}
if(sql.includes('clock_timestamp'))return{rows:[{at:new Date('2026-11-03T12:00:00.000Z')}]}
if(sql.startsWith('DECLARE'))return{rows:[]}
if(sql.startsWith('FETCH'))return{rows:[f.row]}
if(sql.startsWith('CLOSE'))return{rows:[]}
if(sql.startsWith('SELECT c.recovery_operation_id'))return{rows:parent?[{recovery_operation_id:f.intent.operationId,source_operation_id:f.intent.sourceOperationId,profile_version:1}]:[]}
if(sql.startsWith('SELECT r.authority_profile_version'))return{rows:[f.row]}
if(sql.startsWith('SELECT CASE WHEN octet_length(payload'))return{rows:[{sealed:f.row.recovery_payload}]}
if(sql.startsWith('DELETE')){assert(parent);parent=false;deleted.push(f.intent.operationId);return{rowCount:1,rows:[]}}
if(sql.includes('FROM mcp_routing_manual_inverse_charges'))return{rows:parent?[f.parent]:[]}
throw Error('MODEL unexpected fixed query')
}} as unknown as PoolClient
return {db,calls,deleted}
}
test('retention MODEL intent2 authenticates SQL clock after parent GC; unknown noReplay anchor survives',async()=>{
const dir=await mkdtemp(join(tmpdir(),'mm-intent2-retention-')),previous=process.env.MCP_STATE_DIR;process.env.MCP_STATE_DIR=dir
try {
for (const version of [7,8]) {
const f=await retentionFixture(version),owner=retentionOwner(f),before=fingerprint(f.row)
// SQL aging and the transaction owner below are MODELS, never genuine SQL.
await assertLegacyManualHistoryWithinOwner(owner.db,f.profileId,'2026-11-03T12:00:00.000Z',()=>{})
assert.deepEqual(await retainManualInverseChargesWithinOwner(owner.db,1),{deleted:1})
assert.equal(owner.deleted.length,1);assert.equal(fingerprint(f.row),before)
for(const profile of [f.profileId,randomUUID()])await assertLegacyManualHistoryWithinOwner(owner.db,profile,'2026-11-03T12:00:00.000Z',()=>{})
assert(owner.calls.findIndex(s=>s.includes('pg_advisory_xact_lock'))<owner.calls.findIndex(s=>s.startsWith('SELECT c.recovery_operation_id')))
const candidate=owner.calls.find(s=>s.startsWith('SELECT c.recovery_operation_id'))!
assert(candidate.indexOf("state IN('pending','running')")<candidate.indexOf('LIMIT 100'))
assert(candidate.includes('r.authority_profile_version IS NOT NULL'))
assert.equal(f.intent.revision,2);assert.equal(await decryptSecretExisting(f.row.intent_sealed),JSON.stringify(f.intent))
}
}finally{if(previous===undefined)delete process.env.MCP_STATE_DIR;else process.env.MCP_STATE_DIR=previous;await rm(dir,{recursive:true,force:true})}
})
test('retention MODEL refuses recent/future/malformed copy, present corrupt parent, wrong hint; legacy1 remains incompatible with missing parent',async()=>{
const dir=await mkdtemp(join(tmpdir(),'mm-intent2-negative-')),previous=process.env.MCP_STATE_DIR;process.env.MCP_STATE_DIR=dir
try {
const f=await retentionFixture()
await assert.rejects(assertLegacyManualHistoryWithinOwner(retentionOwner(f,false).db,f.profileId,at,()=>{}),/recent/)
await assert.rejects(assertLegacyManualHistoryWithinOwner(retentionOwner(f,false).db,f.profileId,'2026-11-01T11:59:59.999Z',()=>{}))
f.parent.binding_proof='0'.repeat(64)
await assert.rejects(assertLegacyManualHistoryWithinOwner(retentionOwner(f).db,f.profileId,'2026-11-03T12:00:00.000Z',()=>{}),/MAC/)
const wrong=await retentionFixture();wrong.row.authority_profile_version=2;const o=retentionOwner(wrong)
await assert.rejects(retainManualInverseChargesWithinOwner(o.db,1),/hint/);assert.equal(o.deleted.length,0)
const tamper=await retentionFixture();tamper.intent.manualInverseCharge.proof='0'.repeat(64)
tamper.row.intent_sealed=await encryptSecret(JSON.stringify(tamper.intent));tamper.row.intent_proof=await sign(tamper.intent,'route-ai-scoped-recovery-intent-v1')
const t=retentionOwner(tamper);await assert.rejects(retainManualInverseChargesWithinOwner(t.db,1),/MAC/);assert.equal(t.deleted.length,0)
const legacy=await fixture(true,7)
await assert.rejects(assertLegacyManualHistoryWithinOwner(cursor([legacy.row]).db,legacy.profileId,'2026-11-03T12:00:00.000Z',()=>{}),/lacks durable/)
const wrongVersion=await retentionFixture(7);wrongVersion.row.recovery_version=8
const mismatched=retentionOwner(wrongVersion)
await assert.rejects(assertLegacyManualHistoryWithinOwner(mismatched.db,wrongVersion.profileId,'2026-11-03T12:00:00.000Z',()=>{}),/signed allocation/)
await assert.rejects(retainManualInverseChargesWithinOwner(mismatched.db,1),/hint/)
assert.equal(mismatched.deleted.length,0)
// Revision2 payload proof cannot be replaced by an authentic stamp from a different payload.
const payload=await retentionFixture();payload.row.recovery_payload=await encryptSecret('{}')
await assert.rejects(assertLegacyManualHistoryWithinOwner(retentionOwner(payload,false).db,payload.profileId,'2026-11-03T12:00:00.000Z',()=>{}),/payload/)
}finally{if(previous===undefined)delete process.env.MCP_STATE_DIR;else process.env.MCP_STATE_DIR=previous;await rm(dir,{recursive:true,force:true})}
})
test('intent2 checkpoint extraction preserves copy and caps; reserved and nonpersistent legacy keysets remain exact',async()=>{
const dir=await mkdtemp(join(tmpdir(),'mm-intent2-checkpoint-')),previous=process.env.MCP_STATE_DIR;process.env.MCP_STATE_DIR=dir
try {
const f=await retentionFixture(),extra={phase:'RECOVERY_VERIFIED',serverId:1,nativeId:'*1',candidateId:'a',originalRule:'rule',targetProof:'a'.repeat(64),dependencyProof:'b'.repeat(64),sourceEpochs:[],pathCheckId:randomUUID(),observedAt:at}
assert.deepEqual(scopedRecoveryIntentFromCheckpoint({...f.intent,...extra}),f.intent)
const legacy=(await fixture(false,7)).intent
assert.deepEqual(scopedRecoveryIntentFromCheckpoint({...legacy,...extra}),legacy)
const reserved={...legacy,mode:'reserved',reservationId:randomUUID(),networkProof:'c'.repeat(64)}
assert.deepEqual(scopedRecoveryIntentFromCheckpoint({...reserved,...extra}),reserved)
assertPrivateRecoverySize(f.intent,262144)
assert.throws(()=>assertPrivateRecoverySize({...f.intent,oversize:'x'.repeat(196581)},262144),/cap/)
assert.throws(()=>assertPrivateRecoverySize({...f.intent,...extra,originalRule:'\u0000'.repeat(65536)},524288),/cap/)
}finally{if(previous===undefined)delete process.env.MCP_STATE_DIR;else process.env.MCP_STATE_DIR=previous;await rm(dir,{recursive:true,force:true})}
})