Persist configurable connectivity diagnostics
Build and Deploy Gateway / build-and-push (push) Successful in 21s
Build and Deploy Gateway / deploy (push) Successful in 14s

This commit is contained in:
2026-08-19 13:38:19 +03:00
parent df865fbe3d
commit 416b2b294a
22 changed files with 407 additions and 131 deletions
+67
View File
@@ -25,6 +25,73 @@ export const CONNECTIVITY_SITES = Object.freeze([
export const MAX_CUSTOM_DIAGNOSTIC_SERVICES = 5;
export interface DiagnosticService {
id: string;
label: string;
url: string;
}
export interface DiagnosticSettings {
configured: boolean;
customServices: DiagnosticService[];
hiddenServiceIds: string[];
}
function record(value: unknown): Record<string, unknown> {
return value && typeof value === 'object' && !Array.isArray(value)
? value as Record<string, unknown>
: {};
}
function diagnosticService(value: unknown): DiagnosticService | null {
const candidate = record(value);
const id = typeof candidate.id === 'string' ? candidate.id.trim() : '';
const label = typeof candidate.label === 'string' ? candidate.label.trim() : '';
if (!/^custom-[a-z0-9-]{1,80}$/i.test(id) || !label || label.length > 40) return null;
try {
const url = new URL(typeof candidate.url === 'string' ? candidate.url.trim() : '');
if (
url.protocol !== 'https:'
|| url.username
|| url.password
|| (url.port && url.port !== '443')
) return null;
return { id, label, url: url.href };
} catch {
return null;
}
}
export function normalizeDiagnosticSettings(
value: unknown,
{ strict = false }: { strict?: boolean } = {},
): DiagnosticSettings {
const candidate = record(value);
const requestedServices = Array.isArray(candidate.customServices) ? candidate.customServices : [];
const customServices = requestedServices
.map(diagnosticService)
.filter((service): service is DiagnosticService => Boolean(service))
.filter((service, index, services) => services.findIndex(({ id }) => id === service.id) === index)
.slice(0, MAX_CUSTOM_DIAGNOSTIC_SERVICES);
const builtInIds = new Set(CONNECTIVITY_SITES.map(({ id }) => id));
const requestedHiddenIds = Array.isArray(candidate.hiddenServiceIds) ? candidate.hiddenServiceIds : [];
const hiddenServiceIds = requestedHiddenIds
.filter((id): id is string => typeof id === 'string' && builtInIds.has(id))
.filter((id, index, ids) => ids.indexOf(id) === index);
if (strict && (
typeof candidate.configured !== 'boolean'
|| !Array.isArray(candidate.customServices)
|| !Array.isArray(candidate.hiddenServiceIds)
|| customServices.length !== requestedServices.length
|| hiddenServiceIds.length !== requestedHiddenIds.length
)) throw new TypeError('Invalid diagnostic settings');
return {
configured: candidate.configured === true,
customServices,
hiddenServiceIds,
};
}
export interface ConnectivitySiteResult {
id: string;
label: string;
+28 -8
View File
@@ -4,6 +4,10 @@ import {
type RouteRuleOutbound,
} from '../routingRules.js';
import { normalizeServers, resolveServerId } from '../serverIdentity.js';
import {
normalizeDiagnosticSettings,
type DiagnosticSettings,
} from '../connectivityDiagnostics.js';
export type HarborMode = 'client' | 'gateway';
export type ConnectionState = 'running' | 'stopped';
@@ -78,6 +82,7 @@ export interface StateSnapshot {
startedAt: string | null;
lastError: string | null;
};
diagnostics: DiagnosticSettings;
route: {
rulesContractVersion?: typeof ROUTE_RULES_CONTRACT_VERSION;
mode: string;
@@ -116,6 +121,7 @@ export interface PersistedState extends Record<string, unknown> {
routeRulesRevision: number;
connectionDesired?: ConnectionState;
gatewayAutoEnabled?: boolean;
diagnostics: DiagnosticSettings;
}
// Legacy fields are derived in memory for bounded callers during the v5 cutover.
@@ -286,6 +292,7 @@ export function normalizeStoredState(value: unknown): StoredState {
&& Number.isSafeInteger(state.routeRulesRevision) && state.routeRulesRevision >= 0
? state.routeRulesRevision
: 0,
diagnostics: normalizeDiagnosticSettings(state.diagnostics),
subscriptionUrl: selectedProfile?.subscriptionUrl || '',
selectedServerId,
selectedTag: selectedServer?.label || '',
@@ -383,6 +390,7 @@ export function createStateSnapshot({
startedAt: dateOrNull(runtime?.startedAt),
lastError: null,
},
diagnostics: stored.diagnostics,
route: {
rulesContractVersion: ROUTE_RULES_CONTRACT_VERSION,
mode: routeMode,
@@ -418,18 +426,21 @@ export function assertStateSnapshot(snapshot: unknown): StateSnapshot {
? { ...rule, outbound: 'direct' }
: rule
);
const candidate = rawCandidate?.route?.rulesContractVersion === undefined
&& Array.isArray(rawCandidate?.route?.localRules)
&& Array.isArray(rawCandidate?.route?.activeLocalRules)
const candidateWithDiagnostics = rawCandidate && rawCandidate.diagnostics === undefined
? { ...rawCandidate, diagnostics: normalizeDiagnosticSettings(null) }
: rawCandidate;
const candidate = candidateWithDiagnostics?.route?.rulesContractVersion === undefined
&& Array.isArray(candidateWithDiagnostics?.route?.localRules)
&& Array.isArray(candidateWithDiagnostics?.route?.activeLocalRules)
? {
...rawCandidate,
...candidateWithDiagnostics,
route: {
...rawCandidate.route,
localRules: rawCandidate.route.localRules.map(legacyRule),
activeLocalRules: rawCandidate.route.activeLocalRules.map(legacyRule),
...candidateWithDiagnostics.route,
localRules: candidateWithDiagnostics.route.localRules.map(legacyRule),
activeLocalRules: candidateWithDiagnostics.route.activeLocalRules.map(legacyRule),
},
}
: rawCandidate;
: candidateWithDiagnostics;
const validDate = (value: unknown) => typeof value === 'string' && Number.isFinite(Date.parse(value));
const nullableDate = (value: unknown) => value === null || validDate(value);
const nullableString = (value: unknown) => value === null || typeof value === 'string';
@@ -467,6 +478,14 @@ export function assertStateSnapshot(snapshot: unknown): StateSnapshot {
typeof profile.desiredServerId === 'string' &&
Array.isArray(profile.servers) && profile.servers.every(validServer)
);
const validDiagnostics = (diagnostics: DiagnosticSettings) => {
try {
normalizeDiagnosticSettings(diagnostics, { strict: true });
return true;
} catch {
return false;
}
};
if (
!snapshot ||
@@ -496,6 +515,7 @@ export function assertStateSnapshot(snapshot: unknown): StateSnapshot {
!CONNECTION_STATES.has(candidate.connection.process) ||
!nullableDate(candidate.connection.startedAt) ||
!nullableString(candidate.connection.lastError) ||
!validDiagnostics(candidate.diagnostics) ||
!candidate.route ||
![undefined, ROUTE_RULES_CONTRACT_VERSION].includes(candidate.route.rulesContractVersion) ||
typeof candidate.route.mode !== 'string' ||
+3 -3
View File
@@ -1,7 +1,7 @@
export const HARBOR_VERSIONS = Object.freeze({
macClient: '0.26.7',
gatewayClient: '0.27.7',
gatewayBackend: '0.27.0',
macClient: '0.27.0',
gatewayClient: '0.28.0',
gatewayBackend: '0.28.0',
});
export interface ParsedVersion {