151 lines
12 KiB
TypeScript
151 lines
12 KiB
TypeScript
import assert from "node:assert/strict"
|
|
import { randomUUID } from "node:crypto"
|
|
import { setTimeout as delay } from "node:timers/promises"
|
|
import type { Pool } from "pg"
|
|
|
|
const endpoint = new URL(process.env.DATABASE_URL ?? "postgresql://invalid")
|
|
assert.equal(process.env.MMAPP_OWN_FLOW_HOST_KEYS, "true")
|
|
assert.equal(endpoint.hostname, "127.0.0.1"); assert.equal(endpoint.pathname, "/postgres")
|
|
assert.match(process.env.MCP_STATE_DIR ?? "", /mm-mcp-test-/)
|
|
process.env.AUTH_REQUIRED = "true"; process.env.AUTH_ISSUER = "https://auth.test.local"
|
|
const { pool, closePool } = await import("../../db/index.js")
|
|
const { applySqlMigrations } = await import("../../db/migrate.js")
|
|
const { createFlowSettingsStore } = await import("../../services/traffic-flow-settings-store.js")
|
|
const { generateFlowHostKeys, flowSettingsPrivateProof } = await import("../../services/traffic-flow-host-keys-service.js")
|
|
const { invalidateTrafficFlowSettingsCache } = await import("../../services/traffic-flow-settings.js")
|
|
const { withFlowConfigurationLock } = await import("../../services/traffic-flow-host-agent.js")
|
|
const { withApplicationMutationLock } = await import("../../services/config-mutation-lock.js")
|
|
const { getFlowListenerState, getFlowWorkerHealth } = await import("../../services/traffic-flow-ingest.js")
|
|
const { buildApp } = await import("../../index.js")
|
|
const { env } = await import("../../config.js")
|
|
const { createMcpConnection, authenticateMcpToken, updateMcpSettings, transaction } = await import("./store.js")
|
|
const { invokeMcpTool, approvePlan, applyPlan } = await import("./runtime.js")
|
|
const { decryptSecret } = await import("./security.js")
|
|
const actor = { id: "flow-keys-fixture-owner", apps: ["mm"], permissions: ["mm:settings:admin", "mm:traffic:admin"], expiresAt: Date.now() + 3600000 }
|
|
let app: Awaited<ReturnType<typeof buildApp>> | undefined
|
|
let network = 0
|
|
const originalFetch = globalThis.fetch
|
|
globalThis.fetch = (async () => { network++; throw Error("Isolated key generation prohibits network") }) as typeof fetch
|
|
try {
|
|
await applySqlMigrations(pool)
|
|
assert.equal((await pool.query("SELECT count(*)::int AS n FROM servers")).rows[0].n, 0)
|
|
await pool.query("ALTER TABLE traffic_flow_settings ADD COLUMN fixture_future jsonb NOT NULL DEFAULT '{\"opaque\":true}'::jsonb")
|
|
const store = createFlowSettingsStore(pool, async () => {}, invalidateTrafficFlowSettingsCache)
|
|
{
|
|
const before = await store.read(), receiptId = randomUUID(), proof = await flowSettingsPrivateProof(before)
|
|
await store.claim(receiptId, "configuration", proof)
|
|
await assert.rejects(store.claim(receiptId, "configuration", proof))
|
|
await pool.query("UPDATE traffic_flow_settings SET packets_received=111,last_error='telemetry',updated_at=now() WHERE id=1")
|
|
assert.deepEqual(await store.read(), before)
|
|
const desired = { ...before, hostPublicKey: "fixture-public", hostPrivateKey: "fixture-private" }
|
|
assert.equal(await store.compareAndSet(receiptId, proof, before, desired), true)
|
|
assert.deepEqual(await store.read(), desired)
|
|
assert.equal((await pool.query("SELECT packets_received FROM traffic_flow_settings WHERE id=1")).rows[0].packets_received, 111)
|
|
await assert.rejects(store.compareAndSet(receiptId, proof, before, desired))
|
|
}
|
|
{
|
|
const before = await store.read(), id = randomUUID(), proof = await flowSettingsPrivateProof(before)
|
|
await store.claim(id, "configuration", proof)
|
|
await pool.query("UPDATE traffic_flow_settings SET fixture_future='{\"opaque\":false}' WHERE id=1")
|
|
assert.equal(await store.compareAndSet(id, proof, before, { ...before, hostPublicKey: "denied" }), false)
|
|
assert.notEqual((await store.read()).hostPublicKey, "denied")
|
|
}
|
|
{
|
|
const before = await store.read(), id = randomUUID(), proof = await flowSettingsPrivateProof(before)
|
|
await store.claim(id, "configuration", proof)
|
|
let commitAttempts = 0, quarantined = false
|
|
const lost = createFlowSettingsStore({ query: pool.query.bind(pool), connect: async () => {
|
|
const client = await pool.connect()
|
|
return { query: async (sql: string, values?: unknown[]) => {
|
|
const result = await client.query(sql, values)
|
|
if (sql === "COMMIT") { commitAttempts++; throw Error("synthetic lost commit ACK") }
|
|
return result
|
|
}, release: (unsafe?: boolean) => { quarantined = Boolean(unsafe); client.release(unsafe) } }
|
|
} } as unknown as Pick<Pool, "connect" | "query">, async () => {}, invalidateTrafficFlowSettingsCache)
|
|
await assert.rejects(lost.compareAndSet(id, proof, before, { ...before, hostPublicKey: "lost-ack-public" }))
|
|
assert.equal(commitAttempts, 1); assert.equal(quarantined, true)
|
|
assert.equal((await store.read()).hostPublicKey, "lost-ack-public")
|
|
await assert.rejects(store.claim(id, "configuration", proof))
|
|
}
|
|
{
|
|
const before = await store.read(), proof = await flowSettingsPrivateProof(before), attempts = [randomUUID(), randomUUID()]
|
|
await Promise.all(attempts.map(id => store.claim(id, "configuration", proof)))
|
|
const raced = await Promise.all(attempts.map((id, index) => store.compareAndSet(id, proof, before, { ...before, publicEndpoint: `fixture-${index}` })))
|
|
assert.equal(raced.filter(Boolean).length, 1, "full CAS admits only one concurrent different update")
|
|
}
|
|
{
|
|
const before = await store.read(), id = randomUUID(), proof = await flowSettingsPrivateProof(before)
|
|
await store.claim(id, "configuration", proof)
|
|
let revoked = false
|
|
const denied = createFlowSettingsStore({ query: pool.query.bind(pool), connect: async () => {
|
|
const client = await pool.connect()
|
|
return { query: async (sql: string, values?: unknown[]) => {
|
|
const result = await client.query(sql, values)
|
|
if (sql.startsWith("UPDATE traffic_flow_settings")) revoked = true
|
|
return result
|
|
}, release: (unsafe?: boolean) => client.release(unsafe) }
|
|
} } as unknown as Pick<Pool, "connect" | "query">, async () => { if (revoked) throw Error("synthetic revoke after UPDATE") }, invalidateTrafficFlowSettingsCache)
|
|
await assert.rejects(denied.compareAndSet(id, proof, before, { ...before, hostPublicKey: "must-rollback" }))
|
|
assert.deepEqual(await store.read(), before)
|
|
assert.equal((await pool.query("SELECT state FROM flow_settings_intents WHERE receipt_id=$1", [id])).rows[0].state, "claimed")
|
|
await assert.rejects(store.claim(id, "configuration", proof))
|
|
}
|
|
const listener = getFlowListenerState()
|
|
for (const [publicKey, privateKey] of [["", ""], ["", "synthetic-existing-private"], ["synthetic-existing-public", ""], ["", ""]]) {
|
|
await pool.query("UPDATE traffic_flow_settings SET host_public_key=$1,host_private_key=$2 WHERE id=1", [publicKey, privateKey]); invalidateTrafficFlowSettingsCache()
|
|
const before = await store.read(), first = await generateFlowHostKeys()
|
|
assert.equal(first.created, true); assert.match(first.publicKey, /^[A-Za-z0-9+/]{43}=$/)
|
|
const after = await store.read()
|
|
assert.deepEqual({ ...after, hostPublicKey: before.hostPublicKey, hostPrivateKey: before.hostPrivateKey }, before)
|
|
const second = await withApplicationMutationLock(() => withFlowConfigurationLock(() => generateFlowHostKeys()))
|
|
assert.equal(second.created, false); assert.equal(second.publicKey, first.publicKey)
|
|
assert.deepEqual(await store.read(), after)
|
|
assert.ok(!JSON.stringify(first).includes(after.hostPrivateKey))
|
|
}
|
|
const vrfTickets = await (await import("../../services/vrf-production-ticket-store-sql.test.js")).runVrfProductionTicketSqlAcceptance({ sql: pool, transaction, claimConnections: { connect: () => pool.connect() } })
|
|
assert.equal(vrfTickets.passed, true)
|
|
console.log(`VRF production tickets SQL: ${vrfTickets.cases} actual one-use/owner/source/expiry/concurrency/unknown claims; no native execution grant`)
|
|
await updateMcpSettings({ enabled: true }, actor)
|
|
app = await buildApp({ startScheduler: false }); await app.ready()
|
|
const access = await createMcpConnection({ name: "host keys acceptance", mode: "autonomous", permissions: ["mm:traffic:write"], expiresAt: new Date(Date.now() + 600000).toISOString(), scope: { allowGlobal: true, serverIds: [], operationIds: ["traffic.flow.settings.generate-keys"] }, limits: { callsPerMinute: 240, mutationsPerHour: 1000 } }, actor)
|
|
const principal = await authenticateMcpToken(access.token)
|
|
const wait = async (id: string) => {
|
|
for (const deadline = Date.now() + 45000; Date.now() < deadline;) {
|
|
const row = (await pool.query("SELECT state,verification,error_code FROM mcp_operations WHERE id=$1", [id])).rows[0]
|
|
if (!["pending", "running"].includes(row.state)) return row
|
|
await delay(20)
|
|
}
|
|
throw Error("Operation still live, do not replay")
|
|
}
|
|
await pool.query("UPDATE traffic_flow_settings SET host_public_key='',host_private_key='' WHERE id=1"); invalidateTrafficFlowSettingsCache()
|
|
const planned = await invokeMcpTool(principal, "mm_netflow_host_keys_generate_plan", {}) as { planId: string; state: string; changes: unknown }
|
|
assert.equal(planned.state, "awaiting-approval")
|
|
const evidence = JSON.parse(await decryptSecret((await pool.query("SELECT evidence FROM mcp_plans WHERE id=$1", [planned.planId])).rows[0].evidence.sealed))
|
|
assert.equal(evidence.requiresHuman, true); assert.equal(evidence.mutationCost, 1)
|
|
await assert.rejects(applyPlan(principal, planned.planId, randomUUID()))
|
|
await approvePlan(planned.planId, actor, true)
|
|
const operation = await applyPlan(principal, planned.planId, randomUUID()), finished = await wait(operation.id)
|
|
assert.equal(finished.state, "applied", JSON.stringify(finished)); assert.equal(finished.verification, "passed")
|
|
const result = (await pool.query("SELECT result FROM mcp_operation_steps WHERE operation_id=$1", [operation.id])).rows[0].result
|
|
const current = await store.read()
|
|
assert.equal(result.publicKey, current.hostPublicKey); assert.equal(result.created, true)
|
|
assert.ok(!JSON.stringify({ planned, result }).includes(current.hostPrivateKey))
|
|
assert.equal((await pool.query("SELECT count(*)::int AS n FROM flow_settings_intents WHERE receipt_id=$1", [result.receiptId])).rows[0].n, 1)
|
|
await assert.rejects(generateFlowHostKeys({ receiptId: result.receiptId }))
|
|
const jwt = app.jwt.sign({ sub: actor.id, apps: actor.apps, permissions: actor.permissions }, { iss: env.authIssuer, expiresIn: "10m" })
|
|
const response = await app.inject({ method: "POST", url: "/api/traffic/flow/settings/generate-keys", headers: { authorization: "Bearer " + jwt } })
|
|
assert.equal(response.statusCode, 200, response.body)
|
|
assert.equal(response.json().created, false); assert.equal(response.json().publicKey, result.publicKey)
|
|
assert.ok(!response.body.includes(current.hostPrivateKey))
|
|
assert.equal((await app.inject({ method: "POST", url: "/api/traffic/flow/settings/generate-keys" })).statusCode, 401)
|
|
const protectedId = randomUUID()
|
|
await pool.query("INSERT INTO mcp_protections(id,object_ref,created_by) VALUES($1,$2,$3)", [protectedId, { kind: "netflow-settings", id: "1", protection: "deny" }, actor.id])
|
|
const denied = await app.inject({ method: "POST", url: "/api/traffic/flow/settings/generate-keys", headers: { authorization: "Bearer " + jwt } })
|
|
assert.equal(denied.statusCode, 409, denied.body)
|
|
await assert.rejects(invokeMcpTool(principal, "mm_netflow_host_keys_generate_plan", {}))
|
|
await pool.query("DELETE FROM mcp_protections WHERE id=$1", [protectedId])
|
|
assert.equal(network, 0); assert.equal(getFlowWorkerHealth().alive, false); assert.deepEqual(getFlowListenerState(), listener)
|
|
await (await import("./flow-settings-acceptance.js")).acceptMcpFlowSettingsUpdate(actor, app)
|
|
console.log("NetFlow keys SQL/HTTP: complete config CAS incl future columns; telemetry separation; unique claims; lost COMMIT no replay; real generation, reentrant shared locks; mandatory MCP human and private binding; JWT REST parity/protection; zero collector/network")
|
|
} finally { globalThis.fetch = originalFetch; await app?.close(); await closePool() }
|