Files
MikrotikManager/backend/src/modules/mcp/routing-forward-transport-charge.test.cjs
T

100 lines
11 KiB
JavaScript

/* eslint-disable @typescript-eslint/no-require-imports -- CommonJS harness evaluates transpiled modules with explicit dependency adapters. */
// MODEL regression: actual store/guard bodies with inert SQL/MAC adapters.
// Genuine applyPlan/runner/native-boundary cases have separate owned SQL receipts.
const assert = require('node:assert/strict');
const test = require('node:test');
const fs = require('node:fs');
const path = require('node:path');
const { createRequire } = require('node:module');
const { createHash, createHmac } = require('node:crypto');
const root = path.resolve(__dirname, '../../../..');
const dep = createRequire(path.join(root, 'backend/package.json'));
const ts = dep('typescript');
const sha = value => createHash('sha256').update(value).digest('hex');
const mac = value => createHmac('sha256', 'model-only-key-no-host-authority').update(value).digest('hex');
const originalSecurity = fs.readFileSync(path.join(root, 'backend/src/modules/mcp/security.ts'), 'utf8');
const securityAst = ts.createSourceFile('security.ts', originalSecurity, ts.ScriptTarget.Latest, true);
const selected = securityAst.statements.filter(node => ts.isFunctionDeclaration(node) && ['fingerprint', 'redact'].includes(node.name?.text)).map(node => node.getText(securityAst)).join('\n');
const securityExports = {};
const securityBody = ts.transpileModule('const sensitive = /password|passwd|secret|private.?key|preshared|pre.shared|authorization|auth.?key|authentication.?key|tcp.?md5.?key|api.?key|certificate.?contents|token|ciphertext/i; const MAX_MCP_PUBLIC_STRING_LENGTH=16000; const MAX_MCP_PUBLIC_ARRAY_ITEMS=500;\n' + selected, { compilerOptions: { module: ts.ModuleKind.CommonJS } }).outputText;
new Function('exports', 'tokenDigest', securityBody)(securityExports, sha);
const { fingerprint, redact } = securityExports;
class McpError extends Error { constructor(code, message) { super(message); this.code = code; } }
function load(file, providers) {
const output = {};
const source = fs.readFileSync(path.join(__dirname, file), 'utf8');
const compiled = ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS } }).outputText;
new Function('exports', 'require', compiled)(output, name => {
if (name === 'node:crypto') return require(name);
if (name === 'zod') return dep(name);
assert.ok(Object.hasOwn(providers, name), `Unexpected import: ${name}`); return providers[name];
});
return output;
}
const id = n => `00000000-0000-4000-8000-${String(n).padStart(12,'0')}`;
function fixture(service='routing.community') {
const now = new Date('2026-11-02T12:00:00.000Z');
const principal={connectionId:id(3), credentialId:id(4), credentialExpiresAt:'2026-11-02T13:00:00.000Z'};
const authority={packet:{profileId:id(5),profileVersion:7,connectionId:id(3),credentialId:id(4),ownerId:'owner'}, proof:'a'.repeat(64)};
const metadata={operationId:id(1),planId:id(2),operationName:'routing.community.switch',operationVersion:7,ownerId:'owner',phase:'forward',stepIndex:0,serviceName:service,principal,policyVersion:2,generation:'generation',originalExpiresAt:now.getTime()+60000,sealedEvidenceProof:sha('evidence'),sealedPayloadProof:sha('payload'),serviceInput:{profile:{autoRecovery:false},persistentProfileAuthority:authority}};
const source={operation_state:'running',plan_state:'executing',policy_version:2,generation:'generation',approved_by:service==='routing.community.scoped'?'owner':'human',observed_at:now,evidence_sealed:'evidence',payload_sealed:'payload',step_state:'intent',intended_state:redact({kind:'service',service,input:metadata.serviceInput})};
const binding={revision:1,operationId:id(1),planId:id(2),stepIndex:0,profileId:id(5),profileVersion:7,connectionId:id(3),ownerId:'owner',principal,service,operationVersion:7,authorityProof:authority.proof,evidenceProof:fingerprint('evidence'),payloadProof:fingerprint('payload'),scope:[1],createdAt:now.toISOString(),chargeUnits:1};
const row={source_operation_id:id(1),source_plan_id:id(2),source_step_index:0,profile_id:id(5),connection_id:id(3),charge_units:1,binding_sealed:null,binding_proof:null,created_at:now};
let chargeRows=[row], sourceRows=[source], reservedRows=[], decryptCalls=0, nativeWrites=0, keyUnavailable=false;
const queries=[];
function seal() { row.binding_sealed=JSON.stringify(binding);row.binding_proof=mac(fingerprint(binding)); }
seal();
const db={query:async(sql,params)=>{queries.push({sql,params});if(sql.includes('FROM mcp_operations'))return {rows:sourceRows};if(sql.includes('FROM mcp_routing_forward_charges'))return {rows:chargeRows};if(sql.includes('FROM mcp_routing_reservations'))return {rows:reservedRows};throw Error('Unexpected SQL');}};
const providers={
'./routing-manual-inverse-history.js':{},'./routing-manual-inverse-store.js':{},'./routing-switch-ledger-contract.js':{},
'@mmapp/contracts/mcp-routing':{routeAiProfileSchema:{parse:value=>value}},'../../lib/permissions.js':{},
'./routing-profile-authority.js':{routingProfileAuthorityBindingSchema:{parse:value=>value}},
'./routing-community-scoped-request.js':{},'./routing-community-scoped-checkpoint.js':{},'./routing-community-scoped-recovery-source.js':{},
'./security.js':{fingerprint,redact,McpError,decryptSecretExisting:async value=>{decryptCalls++;return value;},privateFingerprinterExisting:async()=>{if(keyUnavailable)throw Error('key unavailable');return mac;}},
'./store.js':{},'./routing-reservation-contract.js':{},'./routing-reservation-transaction.js':{},'./routing-reservation-runtime.js':{}
};
const verifier=load('routing-reservation-store.ts',providers).assertPersistentRoutingForwardTransportCharge;
return {metadata,source,binding,row,queries,seal,
noCharge:()=>{chargeRows=[];},duplicateCharge:()=>{chargeRows=[row,row];},ambiguousSource:()=>{sourceRows=[source,source];},reserved:()=>{reservedRows=[{id:id(8)}];},missingKey:()=>{keyUnavailable=true;},
run:async()=>{await verifier(db,metadata,'owner');nativeWrites++;},writes:()=>nativeWrites,decryptCalls:()=>decryptCalls};
}
for(const service of ['routing.community','routing.community.scoped'])test(`exact ${service} running charge passes without mutation`,async()=>{const f=fixture(service);await f.run();assert.equal(f.writes(),1);assert.equal(f.queries.length,3);assert.ok(f.queries.every(({sql})=>sql.startsWith('SELECT ')));assert.ok(f.queries[0].sql.includes('LIMIT 2'));});
test('scoped transport refuses delegated approval before reading the charge',async()=>{const f=fixture('routing.community.scoped');f.source.approved_by='delegated-admin';await assert.rejects(f.run);assert.equal(f.writes(),0);assert.equal(f.queries.length,1);});
const mutations=[
['missing charge',f=>f.noCharge()],['duplicate charge',f=>f.duplicateCharge()],['reserved overlap',f=>f.reserved()],['ambiguous source',f=>f.ambiguousSource()],
['pending operation',f=>f.source.operation_state='pending'],['applied operation',f=>f.source.operation_state='applied'],['terminal step',f=>f.source.step_state='verified'],['draft plan',f=>f.source.plan_state='draft'],['missing approval',f=>f.source.approved_by=null],['policy drift',f=>f.source.policy_version++],['generation drift',f=>f.source.generation='other'],
['intent drift',f=>f.source.intended_state={}],['bounded-out evidence',f=>f.source.evidence_sealed=null],['bounded-out payload',f=>f.source.payload_sealed=null],['evidence drift',f=>f.source.evidence_sealed='other'],['payload drift',f=>f.source.payload_sealed='other'],['expired original horizon',f=>f.metadata.originalExpiresAt=f.source.observed_at.getTime()],
['missing key',f=>f.missingKey()],['MAC tamper',f=>f.row.binding_proof='0'.repeat(64)],['bounded-out binding',f=>f.row.binding_sealed=null],['SQL profile substitution',f=>f.row.profile_id=id(99)],['SQL units substitution',f=>f.row.charge_units=0],
['signed different profile version',f=>{f.binding.profileVersion++;f.seal();}],['signed different owner',f=>{f.binding.ownerId='other';f.seal();}],['signed different authority',f=>{f.binding.authorityProof='b'.repeat(64);f.seal();}],['signed different credential',f=>{f.binding.principal={...f.binding.principal,credentialId:id(99)};f.seal();}],['signed wrong evidence',f=>{f.binding.evidenceProof='b'.repeat(64);f.seal();}],['future charge',f=>{f.binding.createdAt='2026-11-02T12:00:01.000Z';f.row.created_at=new Date(f.binding.createdAt);f.seal();}],
['reserved profile',f=>f.metadata.serviceInput.profile.autoRecovery=true],['inverse lane',f=>f.metadata.operationName='operations.recover'],['later step',f=>f.metadata.stepIndex=1]
];
for(const [name,mutate] of mutations)test(`${name} refuses before simulated native write`,async()=>{const f=fixture();mutate(f);await assert.rejects(f.run);assert.equal(f.writes(),0);});
test('missing charge refuses before any private binding decryption',async()=>{const f=fixture();f.noCharge();await assert.rejects(f.run);assert.equal(f.decryptCalls(),0);});
function guardFixture() {
const f=fixture();
const input=f.metadata.serviceInput;
Object.assign(input,{nativeId:'*1',originalCandidateId:'a',originalTargetProof:'target-a',dependencyProof:'dep',contextSources:[{serverId:1,proof:'source'}]});
const baseline={nativeId:'*1',candidateId:'a',targetProof:'target-a',dependencyProof:'dep',sourceProofs:input.contextSources};
const row={id:id(5),version:7,connection_id:id(3),grant_expires_at:new Date('2026-11-02T13:00:00.000Z'),config:input.profile,state:{baseline:{}}};
let active=true,chargeChecks=0,failCharge=false,inspections=0;
const providers={
'./routing-main-transition.js':{loadRoutingMainTransitionPreparation:async()=>input},
'./routing-community-scoped-request.js':{assertScopedCommunityRequest:async()=>{}},
'@mmapp/contracts/mcp-routing':{routeAiProfileSchema:{parse:v=>v}},
'../../db/index.js':{pool:{query:async()=>({rows:active?[row]:[]})}},
'../../services/config-mutation-policy.js':{captureVerifiedMcpServiceExecution:async()=>({}),inspectVerifiedMcpServiceExecution:async()=>{inspections++;return f.metadata;}},
'./store.js':{effectiveMcpConnection:async()=>({})},
'./routing-profile-authority.js':{routingProfileAuthorityBindingSchema:{parse:v=>v},assertPersistentScopedProfileAuthority:async()=>{}},
'./routing-community-scoped-recovery-source.js':{},
'./routing-controller-baseline.js':{routingControllerBaselinePrincipal:async()=>({}),authenticateRoutingControllerBaseline:async()=>({value:{packet:baseline}})},
'./security.js':{fingerprint,McpError},
'./routing-reservation-store.js':{assertPersistentRoutingForwardTransportCharge:async(db,m,owner)=>{chargeChecks++;assert.equal(m,f.metadata);assert.equal(owner,'owner');if(failCharge)throw Error('charge absent');}}
};
const guard=load('routing-controller-active-writer.ts',providers).assertActiveRoutingProfileWriter;
return {run:()=>guard(1),metadata:f.metadata,checks:()=>chargeChecks,inspections:()=>inspections,missingCharge:()=>{failCharge=true;},inactive:()=>{active=false;}};
}
for(const service of ['routing.community','routing.community.scoped'])test(`native guard invokes charge verifier for ${service}`,async()=>{const f=guardFixture();f.metadata.serviceName=service;await f.run();assert.equal(f.checks(),1);assert.equal(f.inspections(),2);});
test('charge refusal propagates through native writer guard',async()=>{const f=guardFixture();f.missingCharge();await assert.rejects(f.run);assert.equal(f.checks(),1);assert.equal(f.inspections(),1);});
test('inactive server retains existing nonpersistent behavior',async()=>{const f=guardFixture();f.inactive();await f.run();assert.equal(f.checks(),0);assert.equal(f.inspections(),0);});