Sprint 0 / #19: OpenAPI 3.1 контракт API v1 + TS-клиент и контрактные тесты #57

Merged
embeddevio merged 16 commits from feat/19-openapi into main 2026-10-28 21:21:14 +03:00
Showing only changes of commit 5522f32638 - Show all commits
+281
View File
@@ -309,4 +309,285 @@ export interface paths {
}; };
} }
export type webhooks = Record<string, never>; export type webhooks = Record<string, never>;
export interface components {
schemas: {
/**
* @description Машиночитаемый код ошибки
* @enum {string}
*/
ErrorCode: "bad_request" | "validation_failed" | "unauthorized" | "forbidden" | "not_found" | "conflict" | "rate_limited" | "upstream_unavailable" | "agent_unavailable" | "internal_error";
/** @description Единый формат ошибки для всех не-2xx ответов */
Error: {
code: components["schemas"]["ErrorCode"];
/** @description Человекочитаемое описание */
message: string;
/**
* @description Машиночитаемые детали (для validation_failed — список issues)
* @default null
*/
details: {
[key: string]: unknown;
} | null;
};
Check: {
name: string;
ok: boolean;
detail?: string | null;
};
HealthResponse: {
/** @enum {string} */
status: "ok" | "degraded" | "down";
version: string;
uptime_s: number;
checks: components["schemas"]["Check"][];
};
ReadyResponse: {
ready: boolean;
checks: components["schemas"]["Check"][];
};
/**
* @description Режим split-tunnel
* @enum {string}
*/
RoutingMode: "geoip" | "dns" | "all-vpn" | "direct-only";
/** @enum {string} */
RuleKind: "domain" | "cidr" | "geoip" | "asn";
/** @enum {string} */
RuleAction: "vpn" | "direct";
/** @enum {string} */
LogLevel: "debug" | "info" | "warning" | "error";
/** @enum {string} */
TunnelState: "up" | "down" | "error";
TunnelStatus: {
/** @example awg0 */
interface: string;
state: components["schemas"]["TunnelState"];
handshake_age_s: number | null;
rx_bytes: number;
tx_bytes: number;
endpoint: string | null;
/** Format: date-time */
last_handshake_at: string | null;
};
/** @enum {string} */
GeoIpSource: "ripe-stat" | "cache" | "none";
GeoIpStatus: {
source: components["schemas"]["GeoIpSource"];
prefix_count: number;
/** Format: date-time */
updated_at: string | null;
};
StatusResponse: {
version: string;
uptime_s: number;
mode: components["schemas"]["RoutingMode"];
tunnel: components["schemas"]["TunnelStatus"];
geoip: components["schemas"]["GeoIpStatus"];
kill_switch: boolean;
dns_leak_protection: boolean;
};
TunnelConfig: {
/**
* @description Адрес интерфейса в CIDR
* @example 10.8.0.2/32
*/
address: string;
listen_port: number;
mtu: number;
/** @description Приватный ключ интерфейса; только запись, в ответах отсутствует */
private_key: string;
dns?: string[];
};
PeerConfig: {
public_key: string;
/** @description Preshared key; только запись, в ответах отсутствует */
preshared_key?: string;
endpoint: string;
allowed_ips: string[];
persistent_keepalive?: number | null;
};
DnsConfig: {
/** @enum {string} */
mode: "internal" | "external";
servers: string[];
};
/** @enum {string} */
Ipv6Policy: "direct" | "tunnel" | "block";
RoutingConfig: {
mode: components["schemas"]["RoutingMode"];
kill_switch: boolean;
mtu_clamping: boolean;
ipv6: components["schemas"]["Ipv6Policy"];
geoip: components["schemas"]["GeoIpStatus"];
};
ConfigResponse: {
tunnel: components["schemas"]["TunnelConfig"];
peers: components["schemas"]["PeerConfig"][];
dns: components["schemas"]["DnsConfig"];
routing: components["schemas"]["RoutingConfig"];
/** Format: date-time */
updated_at: string;
};
/** @description Частичное обновление конфигурации (merge в текущее состояние) */
ConfigUpdateRequest: {
tunnel?: components["schemas"]["TunnelConfig"];
peers?: components["schemas"]["PeerConfig"][];
dns?: components["schemas"]["DnsConfig"];
routing?: components["schemas"]["RoutingConfig"];
};
ValidationIssue: {
/** @description JSON Pointer до проблемного поля */
path: string;
code: string;
message: string;
/** @enum {string} */
severity: "error" | "warning";
};
ValidateResponse: {
valid: boolean;
errors: components["schemas"]["ValidationIssue"][];
warnings: components["schemas"]["ValidationIssue"][];
};
RoutingRule: {
/** Format: uuid */
id: string;
name: string;
kind: components["schemas"]["RuleKind"];
value: string;
action: components["schemas"]["RuleAction"];
enabled: boolean;
priority: number;
comment: string | null;
/** Format: date-time */
created_at: string;
/** Format: date-time */
updated_at: string;
};
RoutingRuleCreate: {
name: string;
kind: components["schemas"]["RuleKind"];
value: string;
action: components["schemas"]["RuleAction"];
/** @default true */
enabled: boolean;
/** @default 100 */
priority: number;
comment?: string | null;
};
RoutingRuleUpdate: {
name?: string;
kind?: components["schemas"]["RuleKind"];
value?: string;
action?: components["schemas"]["RuleAction"];
enabled?: boolean;
priority?: number;
comment?: string | null;
};
RoutingRuleList: {
items: components["schemas"]["RoutingRule"][];
total: number;
};
ModeUpdateRequest: {
mode: components["schemas"]["RoutingMode"];
};
ModeUpdateResponse: {
mode: components["schemas"]["RoutingMode"];
/** Format: date-time */
applied_at: string;
/** Format: uuid */
reconcile_id: string;
};
GeoIpUpdateRequest: {
/**
* @description Игнорировать локальный кэш и обновить из RIPE Stat
* @default false
*/
force: boolean;
};
GeoIpUpdateResponse: {
/** @enum {string} */
status: "updated" | "cached" | "failed";
source: components["schemas"]["GeoIpSource"];
prefix_count: number;
/** Format: date-time */
updated_at: string;
duration_ms: number;
};
ImportRequest: {
/** @enum {string} */
format: "amneziawg-conf" | "wireguard-conf" | "json";
content: string;
/**
* @description Применить импорт сразу, а не только вернуть разбор
* @default false
*/
apply: boolean;
};
ImportResponse: {
imported_peers: number;
imported_rules: number;
warnings: string[];
};
Device: {
name: string;
ip: string;
mac: string | null;
vpn: boolean;
ipsets: string[];
/** Format: date-time */
last_seen_at: string | null;
};
DeviceList: {
items: components["schemas"]["Device"][];
total: number;
};
LogEntry: {
/** Format: date-time */
ts: string;
level: components["schemas"]["LogLevel"];
logger: string;
message: string;
fields: {
[key: string]: unknown;
};
};
LogList: {
items: components["schemas"]["LogEntry"][];
total: number;
next_cursor: string | null;
};
AuthLoginRequest: {
username: string;
password: string;
};
AuthSession: {
authenticated: boolean;
username: string | null;
/** Format: date-time */
expires_at: string | null;
csrf_token: string | null;
};
MetricsSample: {
rx_bytes: number;
tx_bytes: number;
rx_rate_bps: number;
tx_rate_bps: number;
handshake_age_s: number | null;
active_peers: number;
};
WsFrame: {
/** @enum {string} */
type: "metrics" | "status" | "error";
/** Format: date-time */
ts: string;
/**
* @description MetricsSample при type=metrics, StatusResponse при type=status,
* Error при type=error.
*/
data: {
[key: string]: unknown;
};
};
};
responses: {
// __CHUNK__ // __CHUNK__