feat(mcp): подготовить сервер и доступ к приёмке

This commit is contained in:
Denozordec committed 2026-11-03 03:12:03 +07:00
commit bf3d50e1ff
3835 files changed
+537122

No files matched your search

+242
View File
@@ -0,0 +1,242 @@
import { ipsecListResponseSchema, ipsecServerSummaryDtoSchema, ipsecUserCreatedSchema, ipsecCertBundleSchema, ipsecExistingCertificateExportIntentSchema, ipsecExistingCertificateExportRequestKeySchema } from "@mmapp/contracts/ipsec"
import { requestJson } from "@/shared/api/http-client"
import { z } from "zod"
import type {
IpsecCertBundle,
IpsecInitRequest,
IpsecListResponse,
IpsecPeerPatch,
IpsecServerSummaryDto,
IpsecUserCreateRequest,
IpsecUserCreated,
IpsecUserPatch,
} from "@mmapp/contracts/ipsec"
export async function listIpsec(
baseUrl: string,
opts?: { serverId?: string },
): Promise<IpsecListResponse> {
const q = new URLSearchParams()
if (opts?.serverId) q.set("serverId", opts.serverId)
const suffix = q.size > 0 ? `?${q.toString()}` : ""
const response = await requestJson<unknown>(baseUrl, `/api/ipsec${suffix}`, { method: "GET" })
const result = ipsecListResponseSchema.safeParse(response)
if (!result.success) throw new Error("Некорректный ответ загрузки IPsec")
const data = result.data
const serverIds = data.servers.map(s => s.serverId)
const clientIds = data.clients.map(c => c.id)
const failedIds = (data.failures ?? []).map(f => f.serverId)
if (new Set(serverIds).size !== serverIds.length || new Set(clientIds).size !== clientIds.length ||
new Set(failedIds).size !== failedIds.length || failedIds.some(id => !id.trim()) ||
data.servers.some(s => s.peers.some(p => p.serverId !== s.serverId) ||
new Set(s.peers.map(p => p.id)).size !== s.peers.length ||
[s.peer, s.pool, s.sharedModeConfig].some(child => child && child.serverId !== s.serverId) ||
new Set((s.certs ?? []).map(c => c.name)).size !== (s.certs ?? []).length) || serverIds.some(id => failedIds.includes(id)) ||
data.clients.some(c => failedIds.includes(c.serverId)) ||
(opts?.serverId && [...serverIds, ...failedIds, ...data.clients.map(c => c.serverId)].some(id => id !== opts.serverId))) {
throw new Error("Несогласованный ответ загрузки IPsec")
}
return data
}
export async function initIpsecServer(
baseUrl: string,
body: IpsecInitRequest,
): Promise<IpsecServerSummaryDto> {
const response = await requestJson<unknown>(baseUrl, "/api/ipsec/server/init", {
method: "POST",
body: JSON.stringify(body),
})
const result = ipsecServerSummaryDtoSchema.safeParse(response)
if (!result.success || result.data.serverId !== body.serverId || !result.data.initialized) {
throw new Error("Сервер не подтвердил инициализацию IKEv2")
}
return result.data
}
export async function deleteIpsecServer(
baseUrl: string,
serverId: string,
opts?: { removeCertificates?: boolean },
): Promise<{ ok: boolean }> {
const q = new URLSearchParams()
if (opts?.removeCertificates === false) q.set("removeCertificates", "false")
const suffix = q.size > 0 ? `?${q.toString()}` : ""
const result = await requestJson<{ ok: boolean }>(
baseUrl,
`/api/ipsec/server/${encodeURIComponent(serverId)}${suffix}`,
{ method: "DELETE" },
)
if (result?.ok !== true) throw new Error("Сервер не подтвердил удаление")
return result
}
export async function createIpsecUser(
baseUrl: string,
body: IpsecUserCreateRequest,
): Promise<IpsecUserCreated> {
const response = await requestJson<unknown>(baseUrl, "/api/ipsec/users", {
method: "POST",
body: JSON.stringify(body),
})
const result = ipsecUserCreatedSchema.safeParse(response)
if (!result.success || result.data.client.serverId !== String(body.serverId) || result.data.client.name !== body.name.trim() || result.data.client.authMethod !== (body.authMethod ?? "certificate")) {
throw new Error("Сервер не подтвердил создание клиента")
}
return result.data
}
export async function patchIpsecUser(
baseUrl: string,
serverId: string,
clientId: string,
patch: IpsecUserPatch,
): Promise<{ ok: boolean }> {
const result = await requestJson<{ ok: boolean }>(
baseUrl,
`/api/ipsec/users/${encodeURIComponent(serverId)}/${encodeURIComponent(clientId)}`,
{ method: "PATCH", body: JSON.stringify(patch) },
)
if (result?.ok !== true) throw new Error("Сервер не подтвердил сохранение клиента")
return result
}
export async function deleteIpsecUser(
baseUrl: string,
serverId: string,
clientId: string,
opts?: { removeCertificate?: boolean },
): Promise<{ ok: boolean }> {
const q = new URLSearchParams()
if (opts?.removeCertificate === false) q.set("removeCertificate", "false")
const suffix = q.size > 0 ? `?${q.toString()}` : ""
const result = await requestJson<{ ok: boolean }>(
baseUrl,
`/api/ipsec/users/${encodeURIComponent(serverId)}/${encodeURIComponent(clientId)}${suffix}`,
{ method: "DELETE" },
)
if (result?.ok !== true) throw new Error("Сервер не подтвердил удаление")
return result
}
function parseExportBundle(response: unknown): IpsecCertBundle {
const result = ipsecCertBundleSchema.safeParse(response)
if (!result.success || !result.data.filename.trim() || !result.data.user.trim() ||
!result.data.passphrase || result.data.passphrase.length < 8 ||
!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(result.data.contentB64) ||
!result.data.contentB64) {
throw new Error("Некорректный ответ экспорта сертификата")
}
return result.data
}
const existingCertExportOptionsSchema = ipsecExistingCertificateExportIntentSchema.extend({
caPrivateKeyExport: z.boolean(),
requestKey: ipsecExistingCertificateExportRequestKeySchema,
}).strict()
export type IpsecExistingCertExportOptions = z.infer<typeof existingCertExportOptionsSchema>
function existingCertExportIntent(passphrase: string, options: IpsecExistingCertExportOptions) {
const parsed = existingCertExportOptionsSchema.safeParse(options)
if (!parsed.success || !z.string().min(8).max(16384).safeParse(passphrase).success) {
throw new Error("Экспорт требует явного подтверждения приватного ключа и UUID запроса")
}
return parsed.data
}
export async function exportIpsecUserCert(
baseUrl: string,
serverId: string,
clientId: string,
passphrase: string,
options: IpsecExistingCertExportOptions,
control?: { signal?: AbortSignal },
): Promise<IpsecCertBundle> {
const intent = existingCertExportIntent(passphrase, options)
const response = await requestJson<unknown>(
baseUrl,
`/api/ipsec/users/${encodeURIComponent(serverId)}/${encodeURIComponent(clientId)}/cert`,
{ method: "POST", signal: control?.signal, headers: { "Idempotency-Key": intent.requestKey }, body: JSON.stringify({ passphrase, privateKeyExport: intent.privateKeyExport, caPrivateKeyExport: intent.caPrivateKeyExport }) },
)
return parseExportBundle(response)
}
export async function exportIpsecCertByName(
baseUrl: string,
serverId: string,
name: string,
passphrase: string,
options: IpsecExistingCertExportOptions,
control?: { signal?: AbortSignal },
): Promise<IpsecCertBundle> {
const intent = existingCertExportIntent(passphrase, options)
const response = await requestJson<unknown>(
baseUrl,
`/api/ipsec/certs/${encodeURIComponent(serverId)}/export`,
{ method: "POST", signal: control?.signal, headers: { "Idempotency-Key": intent.requestKey }, body: JSON.stringify({ name, passphrase, privateKeyExport: intent.privateKeyExport, caPrivateKeyExport: intent.caPrivateKeyExport }) },
)
return parseExportBundle(response)
}
export async function restoreIpsecRevision(
baseUrl: string,
revisionId: string,
serverId: string,
): Promise<{ ok: boolean }> {
return requestJson<{ ok: boolean }>(baseUrl, `/api/ipsec/revisions/${encodeURIComponent(revisionId)}/restore`, {
method: "POST",
body: JSON.stringify({ serverId }),
})
}
export async function patchIpsecPeer(
baseUrl: string,
serverId: string,
peerId: string,
patch: IpsecPeerPatch,
): Promise<{ ok: boolean }> {
const result = await requestJson<{ ok: boolean }>(
baseUrl,
`/api/ipsec/peers/${encodeURIComponent(serverId)}/${encodeURIComponent(peerId)}`,
{ method: "PATCH", body: JSON.stringify(patch) },
)
if (result?.ok !== true) throw new Error("Сервер не подтвердил сохранение peer")
return result
}
export async function deleteIpsecPeer(
baseUrl: string,
serverId: string,
peerId: string,
opts?: { force?: boolean },
): Promise<{ ok: boolean }> {
const q = new URLSearchParams()
if (opts?.force) q.set("force", "true")
const suffix = q.size > 0 ? `?${q.toString()}` : ""
const result = await requestJson<{ ok: boolean }>(
baseUrl,
`/api/ipsec/peers/${encodeURIComponent(serverId)}/${encodeURIComponent(peerId)}${suffix}`,
{ method: "DELETE" },
)
if (result?.ok !== true) throw new Error("Сервер не подтвердил удаление")
return result
}
export async function deleteIpsecCert(
baseUrl: string,
serverId: string,
name: string,
opts?: { force?: boolean },
): Promise<{ ok: boolean }> {
const q = new URLSearchParams()
if (opts?.force) q.set("force", "true")
const suffix = q.size > 0 ? `?${q.toString()}` : ""
const result = await requestJson<{ ok: boolean }>(
baseUrl,
`/api/ipsec/certs/${encodeURIComponent(serverId)}${suffix}`,
{ method: "DELETE", body: JSON.stringify({ name }) },
)
if (result?.ok !== true) throw new Error("Сервер не подтвердил удаление")
return result
}