115 lines
6.3 KiB
TypeScript
115 lines
6.3 KiB
TypeScript
import { isDeepStrictEqual } from "node:util"
|
|
import { trafficFlowSettingsPatchSchema, type TrafficFlowSettingsPatch } from "@mmapp/contracts/traffic-flow"
|
|
|
|
export const flowSettingsPatchSchema = trafficFlowSettingsPatchSchema.strict().refine(value => Object.values(value).some(item => item !== undefined))
|
|
/** Private, complete configuration, including keys and peer JSON. Exclude only ingestion
|
|
* telemetry and updatedAt. Keep unknown future configuration fields in this object. */
|
|
export type FlowSettingsConfiguration = Record<string, unknown> & { hostPublicKey: string; hostPrivateKey: string }
|
|
export type FlowSettingsCommand = { kind: "patch"; patch: TrafficFlowSettingsPatch } | { kind: "ensure-keys" }
|
|
export type FlowSettingsPlan = { command: FlowSettingsCommand; before: FlowSettingsConfiguration }
|
|
export type FlowSettingsPhase = "configuration" | "listener"
|
|
export type FlowSettingsReceipt = {
|
|
status: "applied" | "partial" | "unknown"
|
|
configurationConfirmed: boolean
|
|
listener: "not-requested" | "request-acknowledged" | "unknown"
|
|
listenerReadinessConfirmed: false
|
|
created?: boolean
|
|
publicKey?: string
|
|
attempts: number
|
|
retries: 0
|
|
}
|
|
export type FlowSettingsDependencies = {
|
|
/** Must cover the full server catalog, global permission, protections and live grant. */
|
|
authorize: () => Promise<void>
|
|
/** Serialize with other settings/key/overlay writers, not only this factory. */
|
|
exclusive: <T>(work: () => Promise<T>) => Promise<T>
|
|
readConfiguration: () => Promise<FlowSettingsConfiguration>
|
|
/** Persist one owned intent before dispatch; must refuse an existing/unknown intent. */
|
|
intent: (phase: FlowSettingsPhase) => Promise<void>
|
|
/** Atomic compare-and-set of the complete configuration. False means no write.
|
|
* Timestamp/cache invalidation belong to this adapter. Never retry an exception. */
|
|
compareAndSet: (expected: FlowSettingsConfiguration, desired: FlowSettingsConfiguration) => Promise<boolean>
|
|
generateKeys: () => { publicKey: string; privateKey: string }
|
|
/** The full REST stop/drain/flush/start operation, including enabled=false.
|
|
* The adapter must await fence before each stop/flush/prune/spawn dispatch,
|
|
* including after its internal awaits. An outer fence alone is insufficient. */
|
|
startListener: (fence: () => Promise<void>) => Promise<void>
|
|
}
|
|
const conflict = () => Object.assign(new Error("Настройки NetFlow изменились или не подтверждены"), { statusCode: 409 })
|
|
function configuration(value: FlowSettingsConfiguration): FlowSettingsConfiguration {
|
|
if (!value || typeof value !== "object" || typeof value.hostPublicKey !== "string" || typeof value.hostPrivateKey !== "string") throw conflict()
|
|
return structuredClone(value)
|
|
}
|
|
function command(value: FlowSettingsCommand): FlowSettingsCommand {
|
|
if (value.kind === "ensure-keys") {
|
|
if (Object.keys(value).some(key => key !== "kind")) throw conflict()
|
|
return { kind: "ensure-keys" }
|
|
}
|
|
if (value.kind !== "patch" || Object.keys(value).some(key => !["kind", "patch"].includes(key))) throw conflict()
|
|
const parsed = flowSettingsPatchSchema.parse(value.patch)
|
|
return { kind: "patch", patch: Object.fromEntries(Object.entries(parsed).filter(([, value]) => value !== undefined)) }
|
|
}
|
|
|
|
/** Local candidate only: no runtime imports, registration, default dependencies or
|
|
* secret export. An acknowledged listener request never proves UDP readiness. */
|
|
export function createFlowSettingsExecution(deps: FlowSettingsDependencies) {
|
|
async function prepare(input: FlowSettingsCommand): Promise<FlowSettingsPlan> {
|
|
const parsed = command(input)
|
|
await deps.authorize()
|
|
const before = configuration(await deps.readConfiguration())
|
|
await deps.authorize()
|
|
return { command: parsed, before }
|
|
}
|
|
async function execute(prepared: FlowSettingsPlan): Promise<FlowSettingsReceipt> {
|
|
const plan = { command: command(prepared.command), before: configuration(prepared.before) }
|
|
return deps.exclusive(async () => {
|
|
const fence = async (expected: FlowSettingsConfiguration) => {
|
|
await deps.authorize()
|
|
if (!isDeepStrictEqual(configuration(await deps.readConfiguration()), expected)) throw conflict()
|
|
await deps.authorize()
|
|
}
|
|
await fence(plan.before)
|
|
const keysAlreadyPresent = Boolean(plan.before.hostPublicKey && plan.before.hostPrivateKey)
|
|
if (plan.command.kind === "ensure-keys" && keysAlreadyPresent) return {
|
|
status: "applied", configurationConfirmed: true, listener: "not-requested", listenerReadinessConfirmed: false,
|
|
publicKey: plan.before.hostPublicKey, created: false, attempts: 0, retries: 0,
|
|
}
|
|
const desired = configuration(plan.before)
|
|
if (plan.command.kind === "patch") Object.assign(desired, plan.command.patch)
|
|
else {
|
|
const pair = deps.generateKeys()
|
|
if (typeof pair.publicKey !== "string" || typeof pair.privateKey !== "string" || !pair.publicKey || !pair.privateKey) throw conflict()
|
|
desired.hostPublicKey = pair.publicKey
|
|
desired.hostPrivateKey = pair.privateKey
|
|
}
|
|
await deps.intent("configuration")
|
|
await fence(plan.before)
|
|
const receipt: FlowSettingsReceipt = { status: "unknown", configurationConfirmed: false,
|
|
listener: "not-requested", listenerReadinessConfirmed: false, attempts: 1, retries: 0 }
|
|
try {
|
|
if (!await deps.compareAndSet(plan.before, desired)) throw conflict()
|
|
await fence(desired)
|
|
} catch { return receipt }
|
|
receipt.configurationConfirmed = true
|
|
if (plan.command.kind === "ensure-keys") return { ...receipt, status: "applied", publicKey: desired.hostPublicKey, created: true }
|
|
// Every accepted patch, including display-only and no-op updates, has this
|
|
// effect in REST. A denied listener fence stops here without starting it.
|
|
receipt.status = "partial"
|
|
try {
|
|
await deps.intent("listener")
|
|
await fence(desired)
|
|
} catch { return receipt }
|
|
receipt.listener = "unknown"
|
|
receipt.attempts++
|
|
try {
|
|
await deps.startListener(() => fence(desired))
|
|
await fence(desired)
|
|
receipt.listener = "request-acknowledged"
|
|
receipt.status = "applied"
|
|
} catch { /* No replay or arbitrary exception text in model-facing output. */ }
|
|
return receipt
|
|
})
|
|
}
|
|
return { prepare, execute }
|
|
}
|