fix(evolution): подтверждать сохранение ключа и ответ проверки API

This commit is contained in:
Denozordec committed 2026-10-03 13:16:09 +07:00
1 parent 2b62e0e956
commit 554dba55ec
8 files changed
+103 -111

No files matched your search

+2 -1
View File
@@ -27,7 +27,8 @@
"test:scheduler": "tsx src/services/scheduler-recorded-run.test.ts && tsx src/routes/scheduler.test.ts", "test:scheduler": "tsx src/services/scheduler-recorded-run.test.ts && tsx src/routes/scheduler.test.ts",
"test:backups": "tsx src/services/backup-scheduler-run.test.ts && tsx src/services/backup-restore.test.ts && tsx src/services/backup-deletion.test.ts && tsx src/services/backup-dto.test.ts && tsx src/services/backup-storage-update.test.ts && tsx src/services/backup-job-execution.test.ts && tsx src/routes/backups-operations.test.ts && tsx src/services/s3-backup-client.test.ts", "test:backups": "tsx src/services/backup-scheduler-run.test.ts && tsx src/services/backup-restore.test.ts && tsx src/services/backup-deletion.test.ts && tsx src/services/backup-dto.test.ts && tsx src/services/backup-storage-update.test.ts && tsx src/services/backup-job-execution.test.ts && tsx src/routes/backups-operations.test.ts && tsx src/services/s3-backup-client.test.ts",
"test:live-maps": "tsx src/services/ospf-route-parse.test.ts && tsx src/services/vxlan-live.test.ts && tsx src/services/containers-live.test.ts && tsx src/services/firewall-poll.test.ts && tsx src/services/gre-catalog-poll.test.ts && tsx src/routes/filters-gre-catalog.test.ts && tsx src/routes/bgp-catalog.test.ts && tsx src/services/bgp-catalog-parse.test.ts && tsx src/routes/ospf-catalog.test.ts && tsx src/services/ospf-observed-values.test.ts", "test:live-maps": "tsx src/services/ospf-route-parse.test.ts && tsx src/services/vxlan-live.test.ts && tsx src/services/containers-live.test.ts && tsx src/services/firewall-poll.test.ts && tsx src/services/gre-catalog-poll.test.ts && tsx src/routes/filters-gre-catalog.test.ts && tsx src/routes/bgp-catalog.test.ts && tsx src/services/bgp-catalog-parse.test.ts && tsx src/routes/ospf-catalog.test.ts && tsx src/services/ospf-observed-values.test.ts",
"test": "npm run test:probes && npm run test:exec && npm run test:alert-engine && npm run test:auth && npm run test:wireguard && npm run test:traffic-rate && npm run test:traffic-flow && npm run test:users && npm run test:pg && npm run test:backups && npm run test:scheduler && npm run test:live-maps && npm run test:ospf-optimizer && npm run test:recursive-routes && npm run test:config-sync && npm run test:ros-file && npm run test:containers && npm run test:certificates && npm run test:netflow-settings && npm run test:netflow-purge", "test": "npm run test:probes && npm run test:exec && npm run test:alert-engine && npm run test:auth && npm run test:wireguard && npm run test:traffic-rate && npm run test:traffic-flow && npm run test:users && npm run test:pg && npm run test:backups && npm run test:scheduler && npm run test:live-maps && npm run test:ospf-optimizer && npm run test:recursive-routes && npm run test:config-sync && npm run test:ros-file && npm run test:containers && npm run test:certificates && npm run test:netflow-settings && npm run test:netflow-purge && npm run test:evolution",
"test:evolution": "tsx src/routes/evobgp-settings.test.ts && tsx src/plugins/auth.evolution-settings.test.ts",
"test:netflow-purge": "tsx src/db/operation-lock.test.ts && tsx src/services/traffic-flow-purge-execution.test.ts && tsx src/routes/traffic-flow-purge.test.ts", "test:netflow-purge": "tsx src/db/operation-lock.test.ts && tsx src/services/traffic-flow-purge-execution.test.ts && tsx src/routes/traffic-flow-purge.test.ts",
"test:netflow-settings": "tsx src/services/traffic-flow-host-file-builders.test.ts && tsx src/services/traffic-flow-key-execution.test.ts && tsx src/routes/traffic-flow-settings.test.ts && tsx src/plugins/auth.netflow-settings.test.ts", "test:netflow-settings": "tsx src/services/traffic-flow-host-file-builders.test.ts && tsx src/services/traffic-flow-key-execution.test.ts && tsx src/routes/traffic-flow-settings.test.ts && tsx src/plugins/auth.netflow-settings.test.ts",
"test:geoip": "tsx src/services/traffic-flow-geoip.test.ts && tsx src/plugins/auth.geoip.test.ts", "test:geoip": "tsx src/services/traffic-flow-geoip.test.ts && tsx src/plugins/auth.geoip.test.ts",
@@ -0,0 +1,15 @@
import assert from "node:assert/strict"
import Fastify from "fastify"
import {SignJWT} from "jose"
import {createEvoBgpSettingsRoutes} from "../routes/evobgp-settings.js"
process.env.AUTH_REQUIRED="true";process.env.AUTH_JWT_SECRET="evolution-fixture-secret";process.env.AUTH_ISSUER="https://auth.test.local";process.env.NODE_ENV="test"
const {default:authPlugin,requireAuth}=await import("./auth.js")
const app=Fastify();await app.register(authPlugin);app.addHook("preHandler",requireAuth)
let writes=0,probes=0
await app.register(createEvoBgpSettingsRoutes({read:async()=>({baseUrl:"https://evo.example.test",enabled:false,apiKey:"fixture-only"}),write:async()=>{writes++;return {baseUrl:"https://evo.example.test",enabled:false,apiKey:"fixture-only"}},probe:async()=>{probes++;return {items:[]}}}),{prefix:"/api"})
for(const permission of ["mm:settings:admin","mm:settings:write","mm:traffic:admin","mm:dashboard:read"]){
const token=await new SignJWT({sub:"fixture",apps:["mm"],permissions:[permission]}).setProtectedHeader({alg:"HS256"}).setIssuer("https://auth.test.local").setExpirationTime("1h").sign(new TextEncoder().encode("evolution-fixture-secret"))
for(const [method,url] of [["GET","/api/evobgp/settings"],["PUT","/api/evobgp/settings"],["POST","/api/evobgp/test"]] as const){const response=await app.inject({method,url,headers:{authorization:"Bearer "+token},...(method==="PUT"?{payload:{enabled:false}}:{})});assert.equal(response.statusCode,permission==="mm:settings:admin"?200:403);assert.equal(response.body.includes("fixture-only"),false)}
}
assert.equal(writes,1);assert.equal(probes,1);assert.equal((await app.inject({method:"GET",url:"/api/evobgp/settings"})).statusCode,401)
await app.close();console.log("Evolution JWT gate: actual settings/test handlers require settings admin, no key echo")
@@ -0,0 +1,22 @@
import assert from "node:assert/strict"
import Fastify from "fastify"
import {createEvoBgpSettingsRoutes} from "./evobgp-settings.js"
let probeValue:unknown={items:[]},probes=0
let row={baseUrl:"https://evo.example.test",enabled:false,apiKey:"fixture-existing"},writes=0,fail=false,ignore=false
const app=Fastify();await app.register(createEvoBgpSettingsRoutes({read:async()=>row,probe:async(root,key)=>{probes++;assert.equal(root,row.baseUrl);assert.equal(key,"fixture-test");return probeValue},write:async patch=>{writes++;if(fail)throw Error("DB failure");if(!ignore)row={...row,...patch,apiKey:patch.apiKey===undefined?row.apiKey:patch.apiKey??""};return row}}),{prefix:"/api"})
const put=(payload:unknown)=>app.inject({method:"PUT",url:"/api/evobgp/settings",payload})
let response=await app.inject({method:"GET",url:"/api/evobgp/settings"});assert.deepEqual(response.json(),{baseUrl:row.baseUrl,enabled:false,secretConfigured:true});assert.equal(response.body.includes("fixture-existing"),false)
for(const bad of [{},{unexpected:true},{enabled:"false"},{baseUrl:"ftp://evo.test"},{baseUrl:"https://evo.test/v1"},{apiKey:"fixture\ninvalid"}])assert.equal((await put(bad)).statusCode,400)
assert.equal(writes,0)
response=await put({enabled:false});assert.deepEqual(response.json(),{ok:true,baseUrl:row.baseUrl,enabled:false,secretConfigured:true,savedFields:["enabled"]});assert.equal(row.apiKey,"fixture-existing")
response=await put({baseUrl:"Evo.EXAMPLE.test/",apiKey:"Bearer fixture-new"});assert.equal(response.statusCode,200);assert.equal(row.baseUrl,"https://evo.example.test");assert.equal(row.apiKey,"fixture-new");assert.deepEqual(response.json().savedFields,["baseUrl","apiKey"]);assert.equal(response.body.includes("fixture-new"),false)
response=await put({apiKey:null});assert.equal(response.json().secretConfigured,false);assert.equal(row.apiKey,"")
ignore=true;response=await put({apiKey:"fixture-rejected"});assert.equal(response.statusCode,502);assert.equal(response.body.includes("fixture-rejected"),false)
ignore=false;fail=true;response=await put({enabled:true});assert.equal(response.statusCode,500);assert.equal(writes,5)
fail=false;row={...row,apiKey:"fixture-test"}
const probe=()=>app.inject({method:"POST",url:"/api/evobgp/test",payload:{apiKey:""}})
assert.equal((await probe()).statusCode,200)
for(const raw of [{},null,{items:"bad"},{ok:true}]){probeValue=raw;assert.equal((await probe()).statusCode,502)}
assert.equal(probes,5)
assert.equal((await app.inject({method:"POST",url:"/api/evobgp/test",payload:{baseUrl:"https://evo.test/v1"}})).statusCode,400);assert.equal(probes,5)
await app.close();console.log("Evolution settings HTTP: selective normalized patch, confirmed fields/key without echo, invalid/mismatch/DB error without retry")
+34
View File
@@ -0,0 +1,34 @@
import type {FastifyPluginAsyncZod} from "@fastify/type-provider-zod"
import {z} from "zod"
import {evoBgpTestDraftSchema,normalizeEvoBgpBaseUrl,evoBgpSettingsPatchSchema,evoBgpSettingsSchema,normalizeEvoBgpApiKey,type EvoBgpSavePayload} from "@mmapp/contracts/evobgp"
type SettingsRow = {baseUrl:string;enabled:boolean;apiKey:string}
function publicSettings(row:SettingsRow){
const parsed=evoBgpSettingsSchema.safeParse({baseUrl:row.baseUrl,enabled:row.enabled,secretConfigured:Boolean(normalizeEvoBgpApiKey(row.apiKey))})
if(!parsed.success)throw new Error("Настройки EvoBGP не подтверждены сервером")
return parsed.data
}
export function createEvoBgpSettingsRoutes(deps:{read:()=>Promise<SettingsRow>;write:(patch:EvoBgpSavePayload)=>Promise<SettingsRow>;probe:(root:string,key:string)=>Promise<unknown>}):FastifyPluginAsyncZod {
return async app=>{
app.get("/evobgp/settings",async()=>publicSettings(await deps.read()))
app.post("/evobgp/test",async(req,reply)=>{
const input=evoBgpTestDraftSchema.safeParse(req.body??{})
if(!input.success)return reply.status(400).send({error:input.error.issues[0]?.message??"Укажите допустимые параметры EvoBGP"})
try{
const row=await deps.read(),draft=input.data
const root=normalizeEvoBgpBaseUrl(draft.baseUrl===undefined?row.baseUrl:draft.baseUrl)
const key=normalizeEvoBgpApiKey(draft.apiKey?.trim()?draft.apiKey:row.apiKey)
if(!root||!key)return reply.status(400).send({error:"Нужны URL и API-ключ в форме или сохранённых настройках"})
const raw=await deps.probe(root,key)
if(!z.object({items:z.array(z.unknown())}).safeParse(raw).success) return reply.status(502).send({error:"Соединение с EvoBGP не подтверждено: некорректный ответ API"})
return {ok:true}
}catch(error){return reply.status(502).send({error:error instanceof Error?error.message:"Соединение с EvoBGP не подтверждено"})}
})
app.put("/evobgp/settings",async(req,reply)=>{
const input=evoBgpSettingsPatchSchema.safeParse(req.body)
if(!input.success)return reply.status(400).send({error:input.error.issues[0]?.message??"Укажите допустимые настройки EvoBGP"})
const patch=input.data,row=await deps.write(patch)
if(patch.baseUrl!==undefined&&row.baseUrl!==patch.baseUrl||patch.enabled!==undefined&&row.enabled!==patch.enabled||patch.apiKey!==undefined&&row.apiKey!==(patch.apiKey??""))return reply.status(502).send({error:"Сохранение EvoBGP не подтверждено сервером"})
return {ok:true,...publicSettings(row),savedFields:Object.keys(patch).filter(field=>(patch as Record<string,unknown>)[field]!==undefined)}
})
}
}
+17 -109
View File
@@ -1,48 +1,19 @@
import { eq, sql } from "drizzle-orm" import { eq, sql } from "drizzle-orm"
import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod" import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod"
import { z } from "zod" import { normalizeEvoBgpBaseUrl as normalizeBaseUrl, normalizeEvoBgpApiKey as normalizeApiKey } from "@mmapp/contracts/evobgp"
import { createEvoBgpSettingsRoutes } from "./evobgp-settings.js"
import { db } from "../db/index.js" import { db } from "../db/index.js"
import { evobgpSettings, filterRules } from "../db/schema.js" import { evobgpSettings, filterRules } from "../db/schema.js"
const PutEvobgpSchema = z.object({
baseUrl: z.string().optional(),
enabled: z.boolean().optional(),
/** undefined — не менять; null или "" — очистить ключ; строка — новое значение */
apiKey: z.union([z.string(), z.null()]).optional(),
})
/** Опционально: проверить черновики до сохранения; поле отсутствует — взять из БД */
const TestEvobgpSchema = z.object({
baseUrl: z.string().optional(),
apiKey: z.string().optional(),
})
async function ensureEvobgpRow() { async function ensureEvobgpRow() {
let row = (await db.select().from(evobgpSettings).where(eq(evobgpSettings.id, 1)).limit(1))[0] let row = (await db.select().from(evobgpSettings).where(eq(evobgpSettings.id, 1)).limit(1))[0]
if (!row) { if (!row) {
await db.insert(evobgpSettings).values({ id: 1 }) await db.insert(evobgpSettings).values({ id: 1 }).onConflictDoNothing()
row = (await db.select().from(evobgpSettings).where(eq(evobgpSettings.id, 1)).limit(1))[0] row = (await db.select().from(evobgpSettings).where(eq(evobgpSettings.id, 1)).limit(1))[0]
} }
return row! return row!
} }
function normalizeBaseUrl(raw: string): string {
const trimmed = raw.trim().replace(/\/$/, "")
if (!trimmed) return ""
try {
const u = new URL(trimmed.startsWith("http") ? trimmed : `https://${trimmed}`)
return `${u.protocol}//${u.host}`
} catch {
return trimmed
}
}
/** Сырой API-ключ без префикса Bearer (иначе EvoBGP получит `Bearer Bearer …`). */
function normalizeApiKey(raw: string): string {
const trimmed = raw.trim()
return trimmed.replace(/^Bearer\s+/i, "").trim()
}
interface EvoCatalogRaw { interface EvoCatalogRaw {
modules: { items: Array<{ id: string; name: string; type: string }> } modules: { items: Array<{ id: string; name: string; type: string }> }
domains: { domains: {
@@ -169,83 +140,20 @@ async function credentialsFromDb(): Promise<{ root: string; apiKey: string } | n
} }
const evobgpRoutes: FastifyPluginAsyncZod = async (app) => { const evobgpRoutes: FastifyPluginAsyncZod = async (app) => {
app.get("/evobgp/settings", async (_req, reply) => { await app.register(createEvoBgpSettingsRoutes({
const row = await ensureEvobgpRow() read: ensureEvobgpRow,
return reply.send({ probe: (root,key) => fetchEvoJson<unknown>(root,"/v1/modules?limit=1",key),
baseUrl: row.baseUrl ?? "", write: async patch => {
enabled: Boolean(row.enabled), await ensureEvobgpRow()
secretConfigured: Boolean(normalizeApiKey(row.apiKey ?? "")), const values: {baseUrl?:string;enabled?:boolean;apiKey?:string} = {}
}) if(patch.baseUrl!==undefined) values.baseUrl=patch.baseUrl
}) if(patch.enabled!==undefined) values.enabled=patch.enabled
if(patch.apiKey!==undefined) values.apiKey=patch.apiKey??""
app.put("/evobgp/settings", async (req, reply) => { const [row] = await db.update(evobgpSettings).set({...values,updatedAt:sql`now()`}).where(eq(evobgpSettings.id,1)).returning()
const parsed = PutEvobgpSchema.safeParse(req.body) if(!row) throw new Error("Сохранение EvoBGP не подтверждено сервером")
if (!parsed.success) { return row
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() }) },
} }))
const cur = await ensureEvobgpRow()
let nextBase = cur.baseUrl
let nextEnabled = cur.enabled
let nextKey = cur.apiKey
if (parsed.data.baseUrl !== undefined) {
nextBase = normalizeBaseUrl(parsed.data.baseUrl)
}
if (parsed.data.enabled !== undefined) nextEnabled = parsed.data.enabled
if (parsed.data.apiKey !== undefined) {
nextKey =
parsed.data.apiKey === null || parsed.data.apiKey === ""
? ""
: normalizeApiKey(parsed.data.apiKey)
}
await db.update(evobgpSettings)
.set({
baseUrl: nextBase,
enabled: nextEnabled,
apiKey: nextKey,
updatedAt: sql`now()`,
})
.where(eq(evobgpSettings.id, 1))
const row = await ensureEvobgpRow()
return reply.send({
baseUrl: row.baseUrl ?? "",
enabled: Boolean(row.enabled),
secretConfigured: Boolean(normalizeApiKey(row.apiKey ?? "")),
})
})
/**
* Проверка Bearer: тело опционально — можно передать черновики URL/ключа с формы.
* Для каждого поля: если в JSON ключ отсутствует — используется значение из БД;
* если `apiKey` передана пустой строкой — ключ берётся из БД (проверка только нового URL).
*/
app.post("/evobgp/test", async (req, reply) => {
const parsed = TestEvobgpSchema.safeParse(req.body ?? {})
if (!parsed.success) {
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
}
const row = await ensureEvobgpRow()
const d = parsed.data
const urlRaw = d.baseUrl !== undefined ? d.baseUrl : row.baseUrl
const keyRaw =
d.apiKey !== undefined && d.apiKey.trim() !== "" ? d.apiKey : row.apiKey
const root = normalizeBaseUrl(urlRaw.trim())
const token = normalizeApiKey(keyRaw)
if (!root || !token) {
return reply.status(400).send({
error: "Нужны базовый URL и API-ключ (в форме или уже сохранённые в БД)",
})
}
try {
await fetchEvoJson<{ items: unknown[] }>(root, "/v1/modules?limit=1", token)
return reply.send({ ok: true })
} catch (e) {
const msg = e instanceof Error ? e.message : "Ошибка EvoBGP"
return reply.status(502).send({ error: msg })
}
})
/** /**
* Прокси к GET /v1/router-lists/catalog — учётные данные только из БД. * Прокси к GET /v1/router-lists/catalog — учётные данные только из БД.
+2
View File
@@ -272,3 +272,5 @@ REST запрос → ожидание → подтверждённый output /
Локальная приёмка NetFlow 11Б.3.4.4.8: 19 основных и 10 source E2E, backend JWT с purge, backend/frontend builds и API tests прошли в описанных блоках. Дублирующий purge Settings заменён переходом к единой панели (link E2E/typecheck). Исходные lint ошибки Settings остаются до его блока. Внешние PG/WG/RouterOS/Portal/нестандартный IPv4 prefix не приняты. Далее 11Б.3.5.1 — строгие Evolution settings/save/test API и затем provider/форма. Локальная приёмка NetFlow 11Б.3.4.4.8: 19 основных и 10 source E2E, backend JWT с purge, backend/frontend builds и API tests прошли в описанных блоках. Дублирующий purge Settings заменён переходом к единой панели (link E2E/typecheck). Исходные lint ошибки Settings остаются до его блока. Внешние PG/WG/RouterOS/Portal/нестандартный IPv4 prefix не приняты. Далее 11Б.3.5.1 — строгие Evolution settings/save/test API и затем provider/форма.
Завершён 11Б.3.5.1 — contracts/Evolution strict settings/save/test API: 3 unit, contracts build/typecheck/lint прошли. Новый PUT helper пока не подключён (backend старый receipt). Далее 11Б.3.5.2 backend selective save/подтверждение, затем provider и форма. Завершён 11Б.3.5.1 — contracts/Evolution strict settings/save/test API: 3 unit, contracts build/typecheck/lint прошли. Новый PUT helper пока не подключён (backend старый receipt). Далее 11Б.3.5.2 backend selective save/подтверждение, затем provider и форма.
Завершён 11Б.3.5.2: Evolution backend selective RETURNING save/public receipt и проверка items upstream/test; DI HTTP/JWT, backend build/lint прошли. Старый provider совместим с публичными полями, ещё не мигрирован. Далее 11Б.3.5.3 — provider known/error/time/source; затем форма и upstream catalog validation.
+9 -1
View File
@@ -28,7 +28,7 @@
| 9 | VPN, Firewall и Filters | Завершён в границах локальной приёмки 2026-09-30; блоки 9.1–9.64, ограничения в 9.64 | | 9 | VPN, Firewall и Filters | Завершён в границах локальной приёмки 2026-09-30; блоки 9.1–9.64, ограничения в 9.64 |
| 10 | Маршрутизация, диагностика, карта и терминал | Завершён в границах локальной приёмки 2026-10-01; блоки 10.1–10.71, внешние ограничения в 10.71 | | 10 | Маршрутизация, диагностика, карта и терминал | Завершён в границах локальной приёмки 2026-10-01; блоки 10.1–10.71, внешние ограничения в 10.71 |
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Завершён в границах локальной приёмки 2026-10-01; 11А.1–11А.33, внешние ограничения в 11А.33 | | 11А | Пользователи, контейнеры, сертификаты и резервные копии | Завершён в границах локальной приёмки 2026-10-01; 11А.1–11А.33, внешние ограничения в 11А.33 |
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | В работе: 11Б.1–11Б.3.5.1; scheduler и GeoIP приняты локально; NetFlow принят локально; далее Evolution и общие settings | | 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | В работе: 11Б.1–11Б.3.5.2; scheduler и GeoIP приняты локально; NetFlow принят локально; далее Evolution и общие settings |
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат | | 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
@@ -36,6 +36,14 @@
## Блок 11Б.3.5.2 — backend Evolution save/test (2026-10-03)
- Settings GET/PUT/test вынесены в DI plugin. PUT пишет только переданные нормализованные поля, RETURNING подтверждает их фактические значения, включая apiKey внутри backend; ответ отдаёт только literal ok/public flags/savedFields, без key echo. Singleton insert устойчив к конфликту.
- PUT receipt сохраняет прежние публичные поля верхнего уровня для старого provider, добавляет ok/savedFields. Общий контракт отклоняет пути /v1 вместо прежнего молчаливого удаления пути. Каталог и правила маршрутизации не менялись.
- Проверка соединения подтверждает ожидаемый items array от /v1/modules, а не любой HTTP 200 JSON. Test draft нормализуется общим контрактом; пустой ключ берётся из сохранённой конфигурации, отсутствие URL/key даёт 400. HTTP malformed upstream даёт 502, POST не повторяется.
- DI HTTP и JWT реальных settings/test handlers проверили selective patch, key clear/replace без echo, ignored key 502, DB failure, invalid input/no write, malformed upstream/no success, отсутствие доступа до handler. Backend build и lint прошли; профиль включён в backend test. Реальные PG/Evolution/Portal не проверены.
- Далее 11Б.3.5.3 — provider strict readers/receipts, independent settings/catalog known/error/time, source guards и сохранение последнего каталога. Затем форма Settings/dirty/locks/контекст. Отдельно проверить backend catalog mapper: его upstream optional defaults могут скрывать malformed коллекции.
## Блок 11Б.3.5.1 — строгий Evolution API (2026-10-03) ## Блок 11Б.3.5.1 — строгий Evolution API (2026-10-03)
- Добавлен общий contracts/evobgp: canonical HTTP(S) root URL без пути/query/credentials, явные enabled/secretConfigured, patch/test нормализация ключа без Bearer дублирования, one-line key и strict savedFields receipt. Empty endpoint/key clear и отсутствие поля имеют разную семантику. - Добавлен общий contracts/evobgp: canonical HTTP(S) root URL без пути/query/credentials, явные enabled/secretConfigured, patch/test нормализация ключа без Bearer дублирования, one-line key и strict savedFields receipt. Empty endpoint/key clear и отсутствие поля имеют разную семантику.
+2
View File
@@ -200,3 +200,5 @@ ReUI PRO/base-nova; существующие Frame, Data Grid v9, KpiStatGrid, S
Локальная приёмка NetFlow 11Б.3.4.4.8: 19 основных и 10 source E2E, backend JWT с purge, backend/frontend builds и API tests прошли в описанных блоках. Дублирующий purge Settings заменён переходом к единой панели (link E2E/typecheck). Исходные lint ошибки Settings остаются до его блока. Внешние PG/WG/RouterOS/Portal/нестандартный IPv4 prefix не приняты. Далее 11Б.3.5.1 — строгие Evolution settings/save/test API и затем provider/форма. Локальная приёмка NetFlow 11Б.3.4.4.8: 19 основных и 10 source E2E, backend JWT с purge, backend/frontend builds и API tests прошли в описанных блоках. Дублирующий purge Settings заменён переходом к единой панели (link E2E/typecheck). Исходные lint ошибки Settings остаются до его блока. Внешние PG/WG/RouterOS/Portal/нестандартный IPv4 prefix не приняты. Далее 11Б.3.5.1 — строгие Evolution settings/save/test API и затем provider/форма.
Завершён 11Б.3.5.1 — contracts/Evolution strict settings/save/test API: 3 unit, contracts build/typecheck/lint прошли. Новый PUT helper пока не подключён (backend старый receipt). Далее 11Б.3.5.2 backend selective save/подтверждение, затем provider и форма. Завершён 11Б.3.5.1 — contracts/Evolution strict settings/save/test API: 3 unit, contracts build/typecheck/lint прошли. Новый PUT helper пока не подключён (backend старый receipt). Далее 11Б.3.5.2 backend selective save/подтверждение, затем provider и форма.
Завершён 11Б.3.5.2: Evolution backend selective RETURNING save/public receipt и проверка items upstream/test; DI HTTP/JWT, backend build/lint прошли. Старый provider совместим с публичными полями, ещё не мигрирован. Далее 11Б.3.5.3 — provider known/error/time/source; затем форма и upstream catalog validation.