Files
CDNManager/apps/api/dist/server.js
T
Denozordec 8700dc2957
quality / commitlint (push) Skipped
quality / changes (push) Successful in 5s
quality / docker-check (push) Skipped
CD / update-wiki (push) Successful in 6s
quality / web (push) Successful in 53s
quality / api (push) Successful in 49s
CD / quality (push) Successful in 1m57s
CD / publish (push) Successful in 27s
Enhance CDN Manager: Add Cloudflare API token support, update documentation, and introduce new routes for managing nodes, aliases, and topology. Improve UI components and status badges for better user experience.
2026-09-04 12:49:51 +07:00

1447 lines
46 KiB
JavaScript

// src/server.ts
import { readFileSync, existsSync } from "fs";
import { resolve as resolve3 } from "path";
// src/app.ts
import { resolve as resolve2 } from "path";
import Fastify from "fastify";
import {
serializerCompiler,
validatorCompiler
} from "@fastify/type-provider-zod";
// src/config.ts
import { resolve } from "path";
function boolEnv(v, fallback) {
if (v === void 0 || v === "") return fallback;
return v === "1" || v.toLowerCase() === "true";
}
function loadConfig() {
const isProd = process.env.NODE_ENV === "production";
const jwtSecret = process.env.AUTH_JWT_SECRET ?? process.env.JWT_SECRET ?? (isProd ? "" : "dev-secret-change-me");
return {
databaseUrl: process.env.DATABASE_URL ?? "sqlite:data/app.db",
jwtSecret: jwtSecret || "dev-secret-change-me",
jwtTtlHours: Number(process.env.JWT_TTL_HOURS ?? "24") || 24,
adminUsername: process.env.ADMIN_USERNAME ?? "admin",
adminPasswordHash: process.env.ADMIN_PASSWORD_HASH?.trim() || "devplaceholder",
serverPort: Number(process.env.SERVER_PORT ?? "8081") || 8081,
staticDir: process.env.STATIC_DIR ? resolve(process.env.STATIC_DIR) : null,
logLevel: process.env.LOG_LEVEL ?? "info",
authRequired: boolEnv(process.env.AUTH_REQUIRED, false),
authIssuer: process.env.AUTH_ISSUER ?? process.env.ISSUER ?? "https://auth.shnt.top",
authPortalUrl: (process.env.AUTH_PORTAL_URL ?? process.env.VITE_AUTH_PORTAL_URL ?? "http://localhost:5175").replace(/\/$/, ""),
authAuditIngestSecret: process.env.AUTH_AUDIT_INGEST_SECRET?.trim() || (!isProd ? "dev-audit-ingest-secret" : null),
cloudflareApiToken: (process.env.CLOUDFLARE_API_TOKEN ?? "").trim()
};
}
// src/plugins/auth.ts
import fp from "fastify-plugin";
// src/errors.ts
import { NotFoundError, ConflictError } from "@cdnmanager/db";
import { ValidationError } from "@cdnmanager/shared";
var AppError = class _AppError extends Error {
constructor(code, message, statusCode) {
super(message);
this.code = code;
this.statusCode = statusCode;
this.name = "AppError";
}
code;
statusCode;
static notFound(message) {
return new _AppError("NOT_FOUND", message, 404);
}
static validation(message) {
return new _AppError("VALIDATION_ERROR", message, 400);
}
static unauthorized() {
return new _AppError("UNAUTHORIZED", "unauthorized", 401);
}
static forbidden(message = "forbidden") {
return new _AppError("FORBIDDEN", message, 403);
}
static conflict(message) {
return new _AppError("CONFLICT", message, 409);
}
static cloudflare(message) {
return new _AppError("CLOUDFLARE_ERROR", message, 502);
}
static dnsUpdateFailed(message) {
return new _AppError(
"DNS_UPDATE_FAILED",
message,
502
);
}
static healthcheckCreateFailed(message) {
return new _AppError("HEALTHCHECK_CREATE_FAILED", message, 502);
}
static zoneNotFound(message = "\u0437\u043E\u043D\u0430 Cloudflare \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u0430") {
return new _AppError("ZONE_NOT_FOUND", message, 404);
}
static invalidIp(message = "\u041D\u0435\u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u044B\u0439 IP-\u0430\u0434\u0440\u0435\u0441") {
return new _AppError("INVALID_IP", message, 400);
}
static invalidHostname(message = "\u041D\u0435\u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u043E\u0435 \u0438\u043C\u044F \u0445\u043E\u0441\u0442\u0430") {
return new _AppError("INVALID_HOSTNAME", message, 400);
}
static rateLimited(message = "Cloudflare \u0432\u0440\u0435\u043C\u0435\u043D\u043D\u043E \u043E\u0433\u0440\u0430\u043D\u0438\u0447\u0438\u043B \u0437\u0430\u043F\u0440\u043E\u0441\u044B. \u041F\u043E\u0432\u0442\u043E\u0440\u0438\u0442\u0435 \u043F\u043E\u043F\u044B\u0442\u043A\u0443.") {
return new _AppError("RATE_LIMITED", message, 429);
}
static cloudflareAuthFailed(message = "Cloudflare \u043E\u0442\u043A\u043B\u043E\u043D\u0438\u043B \u0442\u043E\u043A\u0435\u043D \u0434\u043E\u0441\u0442\u0443\u043F\u0430") {
return new _AppError("CLOUDFLARE_AUTH_FAILED", message, 401);
}
static syncFailed(message) {
return new _AppError("SYNC_FAILED", message, 502);
}
static internal(message) {
return new _AppError("INTERNAL_ERROR", message, 500);
}
};
function toAppError(err) {
if (err instanceof AppError) return err;
if (err instanceof NotFoundError) return AppError.notFound(err.message);
if (err instanceof ConflictError) return AppError.conflict(err.message);
if (err instanceof ValidationError) return AppError.validation(err.message);
if (err instanceof Error) return AppError.internal(err.message);
return AppError.internal(String(err));
}
function errorBody(err) {
return {
error: {
code: err.code,
message: err.message
}
};
}
// src/lib/permissions.ts
function hasPermission(granted, required) {
if (granted.includes(required)) return true;
const parts = required.split(":");
if (parts.length !== 3) return false;
const [app2, section, action] = parts;
if (action === "read") {
return granted.includes(`${app2}:${section}:write`) || granted.includes(`${app2}:${section}:admin`);
}
if (action === "write") {
return granted.includes(`${app2}:${section}:admin`);
}
return false;
}
var RULES = [
{
methods: ["GET", "POST", "PUT", "PATCH", "DELETE"],
match: (p) => p.startsWith("/api/v1/settings"),
permission: "cdn:settings:admin"
}
];
function permissionForRequest(method, path) {
const m = method.toUpperCase();
const pathname = path.split("?")[0] ?? path;
for (const rule of RULES) {
if (!rule.methods.includes(m)) continue;
if (rule.match(pathname)) return rule.permission;
}
if (pathname.startsWith("/api/v1/")) return "cdn:dashboard:read";
return null;
}
// src/plugins/auth.ts
async function authPlugin(app2, opts) {
const { config: config2 } = opts;
if (config2.authRequired && (!config2.jwtSecret || config2.jwtSecret.length < 8)) {
throw new Error(
"AUTH_JWT_SECRET / JWT_SECRET required when AUTH_REQUIRED=true"
);
}
await app2.register(import("@fastify/jwt"), {
secret: config2.jwtSecret,
...config2.authRequired ? {
verify: {
allowedIss: [config2.authIssuer]
}
} : {}
});
app2.decorate("config", config2);
if (config2.authRequired) {
app2.log.info(
{ issuer: config2.authIssuer, portal: config2.authPortalUrl },
"AUTH_REQUIRED=true \u2014 portal JWT middleware enabled"
);
} else {
app2.log.info("AUTH_REQUIRED=false \u2014 local JWT / open protected routes with requireAuth");
}
}
async function requireAuth(request, reply) {
const config2 = request.server.config;
const authHeader = request.headers.authorization ?? "";
const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : "";
if (!token) throw AppError.unauthorized();
try {
await request.jwtVerify();
} catch {
throw AppError.unauthorized();
}
if (!config2.authRequired) {
return;
}
const payload = request.user;
const apps = Array.isArray(payload.apps) ? payload.apps.map(String) : [];
const permissions = Array.isArray(payload.permissions) ? payload.permissions.map(String) : [];
if (!apps.includes("cdn")) {
throw AppError.forbidden("\u041D\u0435\u0442 \u0434\u043E\u0441\u0442\u0443\u043F\u0430 \u043A \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044E CDN Manager");
}
request.authUser = {
id: String(payload.sub),
email: String(payload.email ?? ""),
name: String(payload.name ?? ""),
apps,
permissions,
isAdmin: Boolean(payload.is_admin)
};
const required = permissionForRequest(request.method, request.url);
if (required && !hasPermission(permissions, required)) {
throw AppError.forbidden(`\u041D\u0435\u0434\u043E\u0441\u0442\u0430\u0442\u043E\u0447\u043D\u043E \u043F\u0440\u0430\u0432: ${required}`);
}
}
var auth_default = fp(authPlugin, { name: "auth" });
// src/plugins/cors.ts
import fp2 from "fastify-plugin";
async function corsPlugin(app2) {
await app2.register(import("@fastify/cors"), {
origin: true,
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
allowedHeaders: ["Content-Type", "Authorization"]
});
}
var cors_default = fp2(corsPlugin, { name: "cors" });
// src/plugins/cf-client.ts
import fp3 from "fastify-plugin";
// src/lib/cloudflare/http.ts
var CF_API_BASE = "https://api.cloudflare.com/client/v4";
function parseRetryAfter(headers) {
const value = headers.get("retry-after");
if (!value) return null;
const seconds = Number(value);
return Number.isFinite(seconds) ? seconds * 1e3 : null;
}
async function withRetry(operation, maxAttempts = 3) {
let delay = 500;
let lastError;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
return await operation();
} catch (err) {
lastError = err;
if (attempt < maxAttempts - 1) {
await new Promise((r) => setTimeout(r, delay));
delay *= 2;
}
}
}
throw lastError;
}
function mapCloudflareFailure(operation, status, message) {
const lower = message.toLowerCase();
if (status === 401 || status === 403 || lower.includes("authentication")) {
return AppError.cloudflareAuthFailed(
"Cloudflare \u043E\u0442\u043A\u043B\u043E\u043D\u0438\u043B \u0442\u043E\u043A\u0435\u043D. \u041F\u0440\u043E\u0432\u0435\u0440\u044C\u0442\u0435 CLOUDFLARE_API_TOKEN."
);
}
if (status === 429 || lower.includes("rate limit")) {
return AppError.rateLimited();
}
if (lower.includes("zone") && (lower.includes("not found") || status === 404)) {
return AppError.zoneNotFound();
}
if (operation.includes("dns") || operation.includes("dns_record")) {
return AppError.dnsUpdateFailed(
`\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043E\u0431\u043D\u043E\u0432\u0438\u0442\u044C DNS \u0432 Cloudflare: ${message}`
);
}
return AppError.cloudflare(`${operation}: ${message}`);
}
async function handleCfResponse(response, operation) {
if (response.status === 429) {
const wait = parseRetryAfter(response.headers) ?? 5e3;
throw AppError.rateLimited(
`Cloudflare \u0432\u0440\u0435\u043C\u0435\u043D\u043D\u043E \u043E\u0433\u0440\u0430\u043D\u0438\u0447\u0438\u043B \u0437\u0430\u043F\u0440\u043E\u0441\u044B. \u041F\u043E\u0432\u0442\u043E\u0440\u0438\u0442\u0435 \u0447\u0435\u0440\u0435\u0437 ${Math.ceil(wait / 1e3)} \u0441.`
);
}
const body = await response.json();
if (!body.success) {
const msg = body.errors?.map((e) => e.message).join("; ") ?? "unknown cloudflare error";
throw mapCloudflareFailure(operation, response.status, msg);
}
if (body.result === void 0) {
throw mapCloudflareFailure(operation, response.status, "empty result");
}
return body.result;
}
// src/lib/cloudflare/dns-service.ts
function createDnsAdapter(token) {
return {
async listDnsRecords(zoneId) {
return withRetry(async () => {
const all = [];
let page = 1;
while (page <= 50) {
const url = new URL(`${CF_API_BASE}/zones/${zoneId}/dns_records`);
url.searchParams.set("per_page", "100");
url.searchParams.set("page", String(page));
const response = await fetch(url, {
headers: { Authorization: `Bearer ${token}` },
signal: AbortSignal.timeout(3e4)
});
if (response.status >= 500 || response.status === 429) {
throw mapCloudflareFailure(
"list_dns_records",
response.status,
String(response.status)
);
}
const batch = await handleCfResponse(
response,
"list_dns_records"
);
if (batch.length === 0) break;
all.push(...batch);
page += 1;
}
return all;
});
},
async createDnsRecord(zoneId, payload) {
const response = await fetch(`${CF_API_BASE}/zones/${zoneId}/dns_records`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json"
},
body: JSON.stringify(payload),
signal: AbortSignal.timeout(3e4)
});
return handleCfResponse(response, "create_dns_record");
},
async updateDnsRecord(zoneId, recordId, payload) {
const response = await fetch(
`${CF_API_BASE}/zones/${zoneId}/dns_records/${recordId}`,
{
method: "PUT",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json"
},
body: JSON.stringify(payload),
signal: AbortSignal.timeout(3e4)
}
);
return handleCfResponse(response, "update_dns_record");
},
async patchDnsRecord(zoneId, recordId, payload) {
const response = await fetch(
`${CF_API_BASE}/zones/${zoneId}/dns_records/${recordId}`,
{
method: "PATCH",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json"
},
body: JSON.stringify(payload),
signal: AbortSignal.timeout(3e4)
}
);
return handleCfResponse(response, "patch_dns_record");
},
async deleteDnsRecord(zoneId, recordId) {
const response = await fetch(
`${CF_API_BASE}/zones/${zoneId}/dns_records/${recordId}`,
{
method: "DELETE",
headers: { Authorization: `Bearer ${token}` },
signal: AbortSignal.timeout(3e4)
}
);
await handleCfResponse(response, "delete_dns_record");
}
};
}
// src/lib/cloudflare/zone-service.ts
function createZoneAdapter(token) {
return {
async listZones() {
return withRetry(async () => {
const all = [];
let page = 1;
while (true) {
const url = new URL(`${CF_API_BASE}/zones`);
url.searchParams.set("per_page", "50");
url.searchParams.set("page", String(page));
const response = await fetch(url, {
headers: { Authorization: `Bearer ${token}` },
signal: AbortSignal.timeout(3e4)
});
if (response.status >= 500 || response.status === 429) {
throw mapCloudflareFailure(
"list_zones",
response.status,
String(response.status)
);
}
const batch = await handleCfResponse(response, "list_zones");
if (batch.length === 0) break;
all.push(...batch);
if (batch.length < 50) break;
page += 1;
}
return all;
});
},
async getZone(zoneId) {
const response = await fetch(`${CF_API_BASE}/zones/${zoneId}`, {
headers: { Authorization: `Bearer ${token}` },
signal: AbortSignal.timeout(3e4)
});
return handleCfResponse(response, "get_zone");
}
};
}
// src/lib/cf-client.ts
var CloudflareClient = class {
zones;
dns;
token;
constructor(token) {
this.token = token.trim();
this.zones = createZoneAdapter(this.token);
this.dns = createDnsAdapter(this.token);
}
get isConfigured() {
return this.token.length > 0;
}
listZones() {
return this.zones.listZones();
}
getZone(zoneId) {
return this.zones.getZone(zoneId);
}
listDnsRecords(zoneId) {
return this.dns.listDnsRecords(zoneId);
}
createDnsRecord(zoneId, payload) {
return this.dns.createDnsRecord(zoneId, payload);
}
updateDnsRecord(zoneId, recordId, payload) {
return this.dns.updateDnsRecord(zoneId, recordId, payload);
}
patchDnsRecord(zoneId, recordId, payload) {
return this.dns.patchDnsRecord(zoneId, recordId, payload);
}
deleteDnsRecord(zoneId, recordId) {
return this.dns.deleteDnsRecord(zoneId, recordId);
}
};
// src/plugins/cf-client.ts
var cfClientPlugin = async (app2, opts) => {
const cf = new CloudflareClient(opts.config.cloudflareApiToken);
app2.decorate("cf", cf);
};
var cf_client_default = fp3(cfClientPlugin, { name: "cf-client" });
// src/plugins/db.ts
import fp4 from "fastify-plugin";
import {
createDb,
createMemoryDb,
healthCheck,
runMigrations
} from "@cdnmanager/db";
async function dbPlugin(app2, opts) {
const { db, sqlite } = opts.memory ? createMemoryDb() : createDb(opts.config.databaseUrl);
runMigrations(sqlite);
app2.decorate("db", db);
app2.decorate("sqlite", sqlite);
app2.addHook("onClose", async () => {
sqlite.close();
});
}
var db_default = fp4(dbPlugin, { name: "db" });
// src/plugins/error-handler.ts
import fp5 from "fastify-plugin";
async function errorHandlerPlugin(app2) {
app2.setErrorHandler((err, _request, reply) => {
if (reply.sent) return;
const appErr = err.statusCode === 401 ? AppError.unauthorized() : toAppError(err);
reply.status(appErr.statusCode).send(errorBody(appErr));
});
}
var error_handler_default = fp5(errorHandlerPlugin, { name: "error-handler" });
// src/routes/health.ts
import { z } from "zod";
// src/services/auth.ts
import { verify } from "@node-rs/argon2";
async function verifyPassword(config2, password) {
if (config2.adminPasswordHash === "devplaceholder") {
if (password === "admin") return;
throw AppError.unauthorized();
}
const ok = await verify(config2.adminPasswordHash, password);
if (!ok) throw AppError.unauthorized();
}
async function login(config2, sign, req) {
if (req.username !== config2.adminUsername) {
throw AppError.unauthorized();
}
await verifyPassword(config2, req.password);
const expiresAt = new Date(
Date.now() + config2.jwtTtlHours * 60 * 60 * 1e3
);
const token = sign({
sub: req.username,
exp: Math.floor(expiresAt.getTime() / 1e3)
});
return {
token,
expires_at: expiresAt.toISOString()
};
}
// src/routes/health.ts
async function healthRoutes(app2) {
app2.get("/health", async (request) => {
healthCheck(request.server.sqlite);
return { status: "ok" };
});
app2.get("/ready", async (request) => {
healthCheck(request.server.sqlite);
return {
status: "ready",
database: true
};
});
}
async function authRoutes(app2) {
app2.get("/auth/config", async (request) => {
const { config: config2 } = request.server;
return {
required: config2.authRequired,
portal_url: config2.authPortalUrl
};
});
const loginSchema = z.object({
username: z.string(),
password: z.string()
});
app2.post("/auth/login", async (request, reply) => {
if (request.server.config.authRequired) {
return reply.code(403).send({
error: {
code: "FORBIDDEN",
message: "\u041B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 \u0432\u0445\u043E\u0434 \u043E\u0442\u043A\u043B\u044E\u0447\u0451\u043D \u2014 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439\u0442\u0435 auth-portal"
}
});
}
const body = loginSchema.parse(request.body);
const result = await login(
request.server.config,
(payload) => request.server.jwt.sign(payload),
body
);
return result;
});
}
// src/routes/settings.ts
import { appSettingsPatchSchema } from "@cdnmanager/shared";
import { getAppSettings, updateAppSettings } from "@cdnmanager/db";
async function settingsRoutes(app2) {
app2.get("/settings", async (request) => {
const settings = getAppSettings(request.server.db);
return {
...settings,
cloudflareConfigured: request.server.cf.isConfigured
};
});
app2.patch("/settings", async (request) => {
const parsed = appSettingsPatchSchema.safeParse(request.body);
if (!parsed.success) {
throw AppError.validation(
parsed.error.issues[0]?.message ?? "\u043D\u0435\u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u044B\u0435 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438"
);
}
const settings = updateAppSettings(request.server.db, parsed.data);
return {
...settings,
cloudflareConfigured: request.server.cf.isConfigured
};
});
}
// src/routes/fleet.ts
import {
aliasCreateSchema,
aliasPatchSchema,
aliasRetargetSchema,
nodeCreateSchema,
nodePatchSchema,
orphanIgnoreSchema,
syncApplySchema,
zoneCreateSchema,
zonePatchSchema
} from "@cdnmanager/shared";
import {
createAlias,
createNode,
createZone,
dashboardCounts,
deleteAlias,
deleteNode,
deleteZone,
getAlias as getAlias2,
getNode as getNode2,
getZone as getZone2,
ignoreOrphan,
listAliases as listAliases2,
listIgnoredOrphans as listIgnoredOrphans2,
listLocations,
listNodes as listNodes2,
listSyncJobs,
listZones,
unignoreOrphan,
updateAlias as updateAlias2,
updateNode as updateNode2,
updateZone as updateZone2
} from "@cdnmanager/db";
// src/services/naming.ts
function buildHostname(opts) {
const nn = String(opts.indexNum).padStart(2, "0");
const template = opts.template ?? "{loc}-{role}{nn}.{zone}";
let host = template.replaceAll("{loc}", opts.locationCode.toLowerCase()).replaceAll("{role}", opts.role.toLowerCase()).replaceAll("{nn}", nn).replaceAll("{zone}", opts.zoneName.toLowerCase());
if (opts.providerTag) {
host = host.replaceAll("{provider}", opts.providerTag.toLowerCase());
} else {
host = host.replaceAll("-{provider}", "").replaceAll("{provider}", "");
}
return host.replace(/\.$/, "");
}
function normalizeFqdn(name) {
return name.trim().toLowerCase().replace(/\.$/, "");
}
function isValidIpv4(ip) {
const parts = ip.split(".");
if (parts.length !== 4) return false;
return parts.every((p) => {
const n = Number(p);
return Number.isInteger(n) && n >= 0 && n <= 255 && String(n) === p;
});
}
function isValidIpv6(ip) {
return /^[0-9a-f:]+$/i.test(ip) && ip.includes(":");
}
// src/services/sync.ts
import { randomUUID } from "crypto";
import {
getAlias,
getNode,
getZone,
listAliases,
listIgnoredOrphans,
listNodes,
updateAlias,
updateNode,
updateZone,
createSyncJob,
updateSyncJob,
getSyncJob,
addSyncEvent
} from "@cdnmanager/db";
function opId() {
return `op-${randomUUID().slice(0, 8)}`;
}
function findObserved(records, type, name) {
const n = normalizeFqdn(name);
return records.find(
(r) => r.type === type && normalizeFqdn(r.name) === n
);
}
async function buildZoneDiff(db, cf, zoneId) {
const zone = getZone(db, zoneId);
if (!zone.cfZoneId) {
throw AppError.validation("\u0423 \u0437\u043E\u043D\u044B \u043D\u0435 \u0437\u0430\u0434\u0430\u043D cfZoneId Cloudflare");
}
if (!cf.isConfigured) {
throw AppError.cloudflareAuthFailed(
"CLOUDFLARE_API_TOKEN \u043D\u0435 \u0437\u0430\u0434\u0430\u043D. \u0414\u043E\u0431\u0430\u0432\u044C\u0442\u0435 \u0442\u043E\u043A\u0435\u043D \u0432 \u043E\u043A\u0440\u0443\u0436\u0435\u043D\u0438\u0435 API."
);
}
const observed = await cf.listDnsRecords(zone.cfZoneId);
const nodeList = listNodes(db, { zoneId });
const aliasList = listAliases(db, { zoneId });
const ignored = new Set(
listIgnoredOrphans(db, zoneId).map(
(o) => `${o.recordType}:${normalizeFqdn(o.recordName)}`
)
);
const ops = [];
const managedKeys = /* @__PURE__ */ new Set();
for (const node of nodeList) {
const ttl = zone.defaultTtl;
const v4 = node.addresses.find((a) => a.family === "v4");
const v6 = node.addresses.find((a) => a.family === "v6");
if (v4) {
const key = `A:${normalizeFqdn(node.hostname)}`;
managedKeys.add(key);
const obs = findObserved(observed, "A", node.hostname);
const desired = {
type: "A",
name: node.hostname,
content: v4.ip,
ttl,
proxied: false
};
if (!obs) {
ops.push({
id: opId(),
kind: "create",
entityType: "node_a",
entityId: node.id,
recordName: node.hostname,
recordType: "A",
desired,
observed: null,
detail: "A-\u0437\u0430\u043F\u0438\u0441\u044C \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0432 Cloudflare"
});
} else if (obs.content !== v4.ip || obs.proxied === true || obs.ttl !== 1 && obs.ttl !== ttl) {
ops.push({
id: opId(),
kind: obs.proxied === true ? "proxy_violation" : "update",
entityType: "node_a",
entityId: node.id,
recordName: node.hostname,
recordType: "A",
desired,
observed: {
id: obs.id,
type: obs.type,
name: obs.name,
content: obs.content,
ttl: obs.ttl,
proxied: obs.proxied ?? false
},
detail: obs.proxied === true ? "Proxy \u0432\u043A\u043B\u044E\u0447\u0451\u043D \u2014 \u0434\u043B\u044F \u0442\u0443\u043D\u043D\u0435\u043B\u0435\u0439 \u043D\u0443\u0436\u0435\u043D DNS-only" : "\u0421\u043E\u0434\u0435\u0440\u0436\u0438\u043C\u043E\u0435/TTL \u043E\u0442\u043B\u0438\u0447\u0430\u0435\u0442\u0441\u044F \u043E\u0442 desired"
});
} else {
ops.push({
id: opId(),
kind: "noop",
entityType: "node_a",
entityId: node.id,
recordName: node.hostname,
recordType: "A",
desired,
observed: { id: obs.id, content: obs.content }
});
}
}
if (v6) {
const key = `AAAA:${normalizeFqdn(node.hostname)}`;
managedKeys.add(key);
const obs = findObserved(observed, "AAAA", node.hostname);
const desired = {
type: "AAAA",
name: node.hostname,
content: v6.ip,
ttl,
proxied: false
};
if (!obs) {
ops.push({
id: opId(),
kind: "create",
entityType: "node_aaaa",
entityId: node.id,
recordName: node.hostname,
recordType: "AAAA",
desired,
observed: null
});
} else if (obs.content !== v6.ip || obs.proxied === true) {
ops.push({
id: opId(),
kind: obs.proxied === true ? "proxy_violation" : "update",
entityType: "node_aaaa",
entityId: node.id,
recordName: node.hostname,
recordType: "AAAA",
desired,
observed: {
id: obs.id,
content: obs.content,
proxied: obs.proxied ?? false
}
});
}
}
}
for (const alias of aliasList) {
const target = getNode(db, alias.targetNodeId);
const key = `CNAME:${normalizeFqdn(alias.name)}`;
managedKeys.add(key);
const obs = findObserved(observed, "CNAME", alias.name);
const desired = {
type: "CNAME",
name: alias.name,
content: target.hostname,
ttl: zone.defaultTtl,
proxied: false
};
if (!obs) {
ops.push({
id: opId(),
kind: "create",
entityType: "alias",
entityId: alias.id,
recordName: alias.name,
recordType: "CNAME",
desired,
observed: null
});
} else if (normalizeFqdn(obs.content) !== normalizeFqdn(target.hostname) || obs.proxied === true) {
ops.push({
id: opId(),
kind: obs.proxied === true ? "proxy_violation" : "update",
entityType: "alias",
entityId: alias.id,
recordName: alias.name,
recordType: "CNAME",
desired,
observed: {
id: obs.id,
content: obs.content,
proxied: obs.proxied ?? false
}
});
} else {
ops.push({
id: opId(),
kind: "noop",
entityType: "alias",
entityId: alias.id,
recordName: alias.name,
recordType: "CNAME",
desired,
observed: { id: obs.id, content: obs.content }
});
}
}
for (const rec of observed) {
if (!["A", "AAAA", "CNAME"].includes(rec.type)) continue;
const key = `${rec.type}:${normalizeFqdn(rec.name)}`;
if (managedKeys.has(key)) continue;
if (ignored.has(key)) continue;
ops.push({
id: opId(),
kind: "orphan",
entityType: "orphan",
entityId: null,
recordName: rec.name,
recordType: rec.type,
desired: null,
observed: {
id: rec.id,
type: rec.type,
name: rec.name,
content: rec.content,
proxied: rec.proxied ?? false
},
detail: "\u0417\u0430\u043F\u0438\u0441\u044C \u0432 Cloudflare \u0432\u043D\u0435 inventory"
});
}
return ops;
}
async function syncZonePull(db, cf, zoneId) {
const jobId = createSyncJob(db, zoneId);
updateSyncJob(db, jobId, { status: "running" });
try {
const diff = await buildZoneDiff(db, cf, zoneId);
for (const op of diff) {
if (op.kind === "noop") continue;
addSyncEvent(db, jobId, {
kind: op.kind,
recordName: op.recordName,
recordType: op.recordType,
detail: op.detail
});
}
for (const op of diff) {
if (!op.entityId) continue;
const status = op.kind === "noop" ? "ok" : op.kind === "create" ? "missing" : op.kind === "proxy_violation" || op.kind === "update" ? "drift" : "error";
if (op.entityType === "alias") {
updateAlias(db, op.entityId, {
syncStatus: status,
cfRecordId: op.observed?.id ?? getAlias(db, op.entityId).cfRecordId
});
} else if (op.entityType === "node_a") {
updateNode(db, op.entityId, {
syncStatus: status === "ok" ? status : status,
cfARecordId: op.observed?.id ?? getNode(db, op.entityId).cfARecordId
});
} else if (op.entityType === "node_aaaa") {
updateNode(db, op.entityId, {
cfAaaaRecordId: op.observed?.id ?? getNode(db, op.entityId).cfAaaaRecordId,
syncStatus: status
});
}
}
const byEntity = /* @__PURE__ */ new Map();
for (const op of diff) {
if (!op.entityId) continue;
const list = byEntity.get(op.entityId) ?? [];
list.push(op);
byEntity.set(op.entityId, list);
}
for (const [entityId, list] of byEntity) {
const actionable = list.filter((o) => o.kind !== "noop");
if (actionable.length === 0) {
const sample = list[0];
if (sample?.entityType === "alias") {
updateAlias(db, entityId, {
syncStatus: "ok",
cfRecordId: sample.observed?.id ?? void 0,
lastError: null
});
} else if (sample?.entityType.startsWith("node")) {
updateNode(db, entityId, {
syncStatus: "ok",
lastError: null
});
}
}
}
updateSyncJob(db, jobId, {
status: "done",
diffJson: JSON.stringify(diff),
finishedAt: (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19)
});
updateZone(db, zoneId, {
lastSyncAt: (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19)
});
return getSyncJob(db, jobId);
} catch (err) {
updateSyncJob(db, jobId, {
status: "failed",
error: err instanceof Error ? err.message : String(err),
finishedAt: (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19)
});
throw err;
}
}
async function applyZoneDiff(db, cf, zoneId, opIds) {
const zone = getZone(db, zoneId);
if (!zone.cfZoneId) {
throw AppError.validation("\u0423 \u0437\u043E\u043D\u044B \u043D\u0435 \u0437\u0430\u0434\u0430\u043D cfZoneId Cloudflare");
}
const diff = await buildZoneDiff(db, cf, zoneId);
const selected = opIds?.length ? diff.filter((o) => opIds.includes(o.id)) : diff.filter(
(o) => ["create", "update", "proxy_violation", "delete"].includes(o.kind)
);
const jobId = createSyncJob(db, zoneId);
updateSyncJob(db, jobId, { status: "running" });
try {
for (const op of selected) {
if (op.kind === "orphan" || op.kind === "noop") continue;
if (!op.desired) continue;
const payload = {
type: String(op.desired.type),
name: String(op.desired.name),
content: String(op.desired.content),
ttl: Number(op.desired.ttl ?? zone.defaultTtl),
proxied: false
};
if (op.kind === "create") {
const created = await cf.createDnsRecord(zone.cfZoneId, payload);
if (op.entityType === "alias" && op.entityId) {
updateAlias(db, op.entityId, {
syncStatus: "ok",
cfRecordId: created.id ?? null,
lastError: null
});
} else if (op.entityType === "node_a" && op.entityId) {
updateNode(db, op.entityId, {
syncStatus: "ok",
cfARecordId: created.id ?? null,
lastError: null
});
} else if (op.entityType === "node_aaaa" && op.entityId) {
updateNode(db, op.entityId, {
syncStatus: "ok",
cfAaaaRecordId: created.id ?? null,
lastError: null
});
}
} else if ((op.kind === "update" || op.kind === "proxy_violation") && op.observed?.id) {
await cf.updateDnsRecord(
zone.cfZoneId,
String(op.observed.id),
payload
);
if (op.entityType === "alias" && op.entityId) {
updateAlias(db, op.entityId, {
syncStatus: "ok",
cfRecordId: String(op.observed.id),
lastError: null
});
} else if (op.entityType === "node_a" && op.entityId) {
updateNode(db, op.entityId, {
syncStatus: "ok",
cfARecordId: String(op.observed.id),
lastError: null
});
} else if (op.entityType === "node_aaaa" && op.entityId) {
updateNode(db, op.entityId, {
syncStatus: "ok",
cfAaaaRecordId: String(op.observed.id),
lastError: null
});
}
} else if (op.kind === "delete" && op.observed?.id) {
await cf.deleteDnsRecord(zone.cfZoneId, String(op.observed.id));
}
addSyncEvent(db, jobId, {
kind: `applied_${op.kind}`,
recordName: op.recordName,
recordType: op.recordType
});
}
updateSyncJob(db, jobId, {
status: "done",
diffJson: JSON.stringify(selected),
finishedAt: (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19)
});
updateZone(db, zoneId, {
lastSyncAt: (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19)
});
return getSyncJob(db, jobId);
} catch (err) {
updateSyncJob(db, jobId, {
status: "failed",
error: err instanceof Error ? err.message : String(err),
finishedAt: (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19)
});
throw err;
}
}
async function retargetAlias(db, cf, aliasId, targetNodeId) {
const alias = updateAlias(db, aliasId, { targetNodeId });
const zone = getZone(db, alias.zoneId);
const target = getNode(db, targetNodeId);
if (cf.isConfigured && zone.cfZoneId) {
const payload = {
type: "CNAME",
name: alias.name,
content: target.hostname,
ttl: zone.defaultTtl,
proxied: false
};
if (alias.cfRecordId) {
await cf.patchDnsRecord(zone.cfZoneId, alias.cfRecordId, {
content: target.hostname,
proxied: false
});
updateAlias(db, aliasId, { syncStatus: "ok", lastError: null });
} else {
const created = await cf.createDnsRecord(zone.cfZoneId, payload);
updateAlias(db, aliasId, {
syncStatus: "ok",
cfRecordId: created.id ?? null,
lastError: null
});
}
} else {
updateAlias(db, aliasId, { syncStatus: "pending" });
}
return getAlias(db, aliasId);
}
function exportBindZone(db, zoneId) {
const zone = getZone(db, zoneId);
const nodeList = listNodes(db, { zoneId });
const aliasList = listAliases(db, { zoneId });
const lines = [
`;; CDNManager export \u2014 ${zone.name}`,
`;; TTL default ${zone.defaultTtl}; all records DNS-only (proxied:false)`,
"",
";; Canonical A/AAAA"
];
for (const node of nodeList) {
const v4 = node.addresses.find((a) => a.family === "v4");
const v6 = node.addresses.find((a) => a.family === "v6");
if (v4) {
lines.push(
`${node.hostname}. ${zone.defaultTtl} IN A ${v4.ip} ; cf_tags=cf-proxied:false`
);
}
if (v6) {
lines.push(
`${node.hostname}. ${zone.defaultTtl} IN AAAA ${v6.ip} ; cf_tags=cf-proxied:false`
);
}
}
lines.push("", ";; Service CNAME aliases");
for (const alias of aliasList) {
lines.push(
`${alias.name}. ${zone.defaultTtl} IN CNAME ${alias.targetHostname}. ; purpose=${alias.purpose}`
);
}
lines.push("");
return lines.join("\n");
}
// src/routes/fleet.ts
var fleetRoutes = async (app2) => {
app2.get("/locations", async () => listLocations(app2.db));
app2.get("/zones", async () => listZones(app2.db));
app2.post("/zones", async (req) => {
const body = zoneCreateSchema.parse(req.body);
return createZone(app2.db, body);
});
app2.get(
"/zones/:zoneId",
async (req) => getZone2(app2.db, req.params.zoneId)
);
app2.patch("/zones/:zoneId", async (req) => {
const body = zonePatchSchema.parse(req.body);
return updateZone2(app2.db, req.params.zoneId, body);
});
app2.delete("/zones/:zoneId", async (req) => {
deleteZone(app2.db, req.params.zoneId);
return { ok: true };
});
app2.get("/cloudflare/zones", async () => {
if (!app2.cf.isConfigured) {
throw AppError.cloudflareAuthFailed("CLOUDFLARE_API_TOKEN \u043D\u0435 \u0437\u0430\u0434\u0430\u043D");
}
return app2.cf.listZones();
});
app2.post(
"/zones/:zoneId/sync",
async (req) => syncZonePull(app2.db, app2.cf, req.params.zoneId)
);
app2.post(
"/zones/:zoneId/apply",
async (req) => {
const body = syncApplySchema.parse(req.body ?? {});
return applyZoneDiff(app2.db, app2.cf, req.params.zoneId, body.opIds);
}
);
app2.get(
"/zones/:zoneId/sync-jobs",
async (req) => listSyncJobs(app2.db, req.params.zoneId)
);
app2.get(
"/zones/:zoneId/export/bind",
async (req) => ({
zoneName: getZone2(app2.db, req.params.zoneId).name,
content: exportBindZone(app2.db, req.params.zoneId)
})
);
app2.get(
"/zones/:zoneId/orphans/ignored",
async (req) => listIgnoredOrphans2(app2.db, req.params.zoneId)
);
app2.post(
"/zones/:zoneId/orphans/ignore",
async (req) => {
const body = orphanIgnoreSchema.parse(req.body);
ignoreOrphan(
app2.db,
req.params.zoneId,
normalizeFqdn(body.recordName),
body.recordType.toUpperCase()
);
return { ok: true };
}
);
app2.post(
"/zones/:zoneId/orphans/unignore",
async (req) => {
const body = orphanIgnoreSchema.parse(req.body);
unignoreOrphan(
app2.db,
req.params.zoneId,
normalizeFqdn(body.recordName),
body.recordType.toUpperCase()
);
return { ok: true };
}
);
app2.get("/nodes", async (req) => {
const q = req.query;
return listNodes2(app2.db, {
zoneId: q.zoneId,
locationId: q.locationId,
role: q.role,
syncStatus: q.syncStatus,
q: q.q
});
});
app2.post("/nodes", async (req) => {
const body = nodeCreateSchema.parse(req.body);
if (!isValidIpv4(body.ipv4)) throw AppError.invalidIp("\u041D\u0435\u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u044B\u0439 IPv4");
if (body.ipv6 && !isValidIpv6(body.ipv6)) {
throw AppError.invalidIp("\u041D\u0435\u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u044B\u0439 IPv6");
}
const zone = getZone2(app2.db, body.zoneId);
const loc = listLocations(app2.db).find((l) => l.id === body.locationId);
if (!loc) throw AppError.notFound("location not found");
const hostname = body.hostname?.trim() || buildHostname({
template: zone.namingTemplate,
locationCode: loc.code,
role: body.role,
indexNum: body.indexNum,
zoneName: zone.name,
providerTag: body.providerTag
});
return createNode(app2.db, {
zoneId: body.zoneId,
locationId: body.locationId,
hostname: normalizeFqdn(hostname),
role: body.role,
indexNum: body.indexNum,
providerTag: body.providerTag,
notes: body.notes,
ipv4: body.ipv4,
ipv6: body.ipv6
});
});
app2.get(
"/nodes/:nodeId",
async (req) => getNode2(app2.db, req.params.nodeId)
);
app2.patch("/nodes/:nodeId", async (req) => {
const body = nodePatchSchema.parse(req.body);
if (body.ipv4 && !isValidIpv4(body.ipv4)) {
throw AppError.invalidIp("\u041D\u0435\u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u044B\u0439 IPv4");
}
if (body.ipv6 && !isValidIpv6(body.ipv6)) {
throw AppError.invalidIp("\u041D\u0435\u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u044B\u0439 IPv6");
}
return updateNode2(app2.db, req.params.nodeId, {
...body,
hostname: body.hostname ? normalizeFqdn(body.hostname) : void 0
});
});
app2.delete("/nodes/:nodeId", async (req) => {
deleteNode(app2.db, req.params.nodeId);
return { ok: true };
});
app2.get("/aliases", async (req) => {
const q = req.query;
return listAliases2(app2.db, {
zoneId: q.zoneId,
purpose: q.purpose,
syncStatus: q.syncStatus,
q: q.q
});
});
app2.post("/aliases", async (req) => {
const body = aliasCreateSchema.parse(req.body);
return createAlias(app2.db, {
zoneId: body.zoneId,
name: normalizeFqdn(body.name),
purpose: body.purpose,
mode: body.mode,
targetNodeId: body.targetNodeId
});
});
app2.get(
"/aliases/:aliasId",
async (req) => getAlias2(app2.db, req.params.aliasId)
);
app2.patch(
"/aliases/:aliasId",
async (req) => {
const body = aliasPatchSchema.parse(req.body);
return updateAlias2(app2.db, req.params.aliasId, {
...body,
name: body.name ? normalizeFqdn(body.name) : void 0
});
}
);
app2.post(
"/aliases/:aliasId/retarget",
async (req) => {
const body = aliasRetargetSchema.parse(req.body);
return retargetAlias(
app2.db,
app2.cf,
req.params.aliasId,
body.targetNodeId
);
}
);
app2.delete(
"/aliases/:aliasId",
async (req) => {
deleteAlias(app2.db, req.params.aliasId);
return { ok: true };
}
);
app2.get("/dashboard/stats", async () => {
const base = dashboardCounts(app2.db);
const nodes = listNodes2(app2.db);
const aliases = listAliases2(app2.db);
const proxyViolations = [...nodes, ...aliases].filter(
(x) => x.syncStatus === "drift" && (x.lastError ?? "").includes("Proxy")
).length;
const orphans = listZones(app2.db).length ? listSyncJobs(app2.db, listZones(app2.db)[0].id).flatMap((j) => j.diff ?? []).filter((o) => o.kind === "orphan").length : 0;
return {
nodes: base.nodes,
aliases: base.aliases,
syncOk: base.syncOk,
drift: base.drift,
proxyViolations,
orphans,
lastSyncAt: base.lastSyncAt,
nodesWithoutIp: base.nodesWithoutIp,
brokenAliases: base.brokenAliases
};
});
app2.get("/topology", async (req) => {
const q = req.query;
const zoneId = q.zoneId;
const locs = listLocations(app2.db);
const nodeList = listNodes2(app2.db, zoneId ? { zoneId } : {});
const aliasList = listAliases2(app2.db, zoneId ? { zoneId } : {});
return {
locations: locs,
nodes: nodeList.map((n) => ({
id: n.id,
hostname: n.hostname,
role: n.role,
locationCode: n.locationCode ?? "",
ipv4: n.addresses.find((a) => a.family === "v4")?.ip ?? null,
syncStatus: n.syncStatus
})),
edges: aliasList.map((a) => ({
id: a.id,
aliasName: a.name,
purpose: a.purpose,
fromNodeId: a.targetNodeId,
toHostname: a.targetHostname ?? ""
}))
};
});
app2.get("/naming/preview", async (req) => {
const q = req.query;
if (!q.zoneId || !q.locationId || !q.role) {
throw AppError.validation("zoneId, locationId, role \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u044B");
}
const zone = getZone2(app2.db, q.zoneId);
const loc = listLocations(app2.db).find((l) => l.id === q.locationId);
if (!loc) throw AppError.notFound("location not found");
const indexNum = Number(q.indexNum ?? "1") || 1;
return {
hostname: buildHostname({
template: q.template || zone.namingTemplate,
locationCode: loc.code,
role: q.role,
indexNum,
zoneName: zone.name,
providerTag: q.providerTag
})
};
});
};
// src/app.ts
async function buildApp(opts = {}) {
const config2 = opts.config ?? loadConfig();
const app2 = Fastify({
logger: { level: config2.logLevel }
}).withTypeProvider();
app2.setValidatorCompiler(validatorCompiler);
app2.setSerializerCompiler(serializerCompiler);
await app2.register(import("@fastify/sensible"));
await app2.register(import("@fastify/helmet"), {
contentSecurityPolicy: false
});
await app2.register(import("@fastify/rate-limit"), {
max: 300,
timeWindow: "1 minute"
});
await app2.register(cors_default);
await app2.register(error_handler_default);
await app2.register(db_default, { config: config2, memory: opts.memory });
await app2.register(cf_client_default, { config: config2 });
await app2.register(auth_default, { config: config2 });
await app2.register(healthRoutes);
await app2.register(authRoutes, { prefix: "/api/v1" });
await app2.register(
async (protectedApi) => {
protectedApi.addHook("onRequest", requireAuth);
await protectedApi.register(settingsRoutes);
await protectedApi.register(fleetRoutes);
},
{ prefix: "/api/v1" }
);
const staticDir = config2.staticDir ?? resolve2(process.cwd(), "static");
if (config2.staticDir !== null) {
await app2.register(import("@fastify/static"), {
root: staticDir,
wildcard: false
});
app2.setNotFoundHandler(async (_request, reply) => {
return reply.sendFile("index.html");
});
}
return app2;
}
// src/server.ts
for (const path of [
resolve3(import.meta.dirname, "../../../.env"),
".env",
"../.env"
]) {
if (!existsSync(path)) continue;
const content = readFileSync(path, "utf-8");
for (const line of content.split("\n")) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
const eq = trimmed.indexOf("=");
if (eq === -1) continue;
const key = trimmed.slice(0, eq).trim();
let value = trimmed.slice(eq + 1).trim();
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
value = value.slice(1, -1);
}
if (!(key in process.env)) process.env[key] = value;
}
break;
}
var config = loadConfig();
var app = await buildApp({ config });
try {
await app.listen({ port: config.serverPort, host: "0.0.0.0" });
app.log.info(`listening on ${config.serverPort}`);
} catch (err) {
app.log.error(err);
process.exit(1);
}