@qpjoy/electron-launcher 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,119 @@
1
+ import type { LauncherRoutePlan } from '@qpjoy/mx-launcher-core';
2
+ import { type ElectronLauncherNetworkOwnershipClaim, type ElectronLauncherNetworkOwnershipRegistry, type ElectronLauncherNetworkOwnershipRoute } from './network-ownership-registry.js';
3
+ import { connectLauncherWireGuardPeer, stopLauncherWireGuardPeer, type ElectronLauncherWireGuardPathPreference, type ElectronLauncherWireGuardRuntimeOptions } from './wireguard.js';
4
+ export type ElectronLauncherStandaloneDataPlaneState = 'lease-missing' | 'lease-active' | 'data-plane-pending' | 'proxy-tun-captured' | 'ownership-conflict' | 'route-mismatch' | 'service-unreachable' | 'network-ready';
5
+ export type ElectronLauncherStandaloneDataPlaneSeverity = 'ok' | 'info' | 'warning' | 'error';
6
+ export type ElectronLauncherStandaloneDataPlaneProbeTarget = 'lease-ip' | 'internal-control' | 'domestic-gateway' | 'service-vip' | 'dns-server';
7
+ export interface ElectronLauncherStandaloneDataPlaneInput {
8
+ routePlan?: LauncherRoutePlan | null;
9
+ leaseIp?: string | null;
10
+ serviceVip?: string | null;
11
+ dnsServer?: string | null;
12
+ internalControlIp?: string | null;
13
+ domesticGatewayIp?: string | null;
14
+ endpoint?: string | null;
15
+ requiredProbeTargets?: ElectronLauncherStandaloneDataPlaneProbeTarget[] | null;
16
+ expectedInterfaceName?: string | null;
17
+ expectedInterfaceAddresses?: string[] | null;
18
+ wireGuardActive?: boolean | null;
19
+ }
20
+ export interface ElectronLauncherStandaloneOwnershipInput {
21
+ ownerId?: string | null;
22
+ productId?: string | null;
23
+ instanceId?: string | null;
24
+ displayName?: string | null;
25
+ priority?: number | null;
26
+ metadata?: Record<string, unknown> | null;
27
+ dnsHosts?: string[] | null;
28
+ dnsZones?: string[] | null;
29
+ routeCidrs?: string[] | null;
30
+ reverseProxyRoutes?: ElectronLauncherNetworkOwnershipRoute[] | null;
31
+ existingClaims?: ElectronLauncherNetworkOwnershipClaim[] | null;
32
+ }
33
+ export interface ElectronLauncherStandaloneOwnershipState {
34
+ statePath: string;
35
+ claims: ElectronLauncherNetworkOwnershipClaim[];
36
+ registry: ElectronLauncherNetworkOwnershipRegistry;
37
+ updatedAt: string;
38
+ }
39
+ export interface ElectronLauncherStandaloneDataPlaneApplyInput extends ElectronLauncherWireGuardRuntimeOptions, ElectronLauncherStandaloneOwnershipInput {
40
+ routePlan: LauncherRoutePlan;
41
+ privateKey: string;
42
+ dnsDomains?: string[] | null;
43
+ suppressWireGuardDns?: boolean | null;
44
+ mtu?: number | null;
45
+ pathPreference?: ElectronLauncherWireGuardPathPreference;
46
+ action?: 'up' | 'restart';
47
+ requiredProbeTargets?: ElectronLauncherStandaloneDataPlaneProbeTarget[] | null;
48
+ failOnOwnershipConflicts?: boolean | null;
49
+ dataPlaneProbeAttempts?: number | null;
50
+ dataPlaneProbeIntervalMs?: number | null;
51
+ ownershipStatePath?: string | null;
52
+ skipOwnershipState?: boolean | null;
53
+ }
54
+ export interface ElectronLauncherStandaloneDataPlaneApplyResult {
55
+ ok: boolean;
56
+ state: ElectronLauncherStandaloneDataPlaneState;
57
+ diagnostics: ElectronLauncherStandaloneDataPlaneDiagnostics;
58
+ ownershipClaim: ElectronLauncherNetworkOwnershipClaim;
59
+ ownershipRegistry: ElectronLauncherNetworkOwnershipRegistry;
60
+ wireGuard: Awaited<ReturnType<typeof connectLauncherWireGuardPeer>> | null;
61
+ message: string;
62
+ }
63
+ export interface ElectronLauncherStandaloneDataPlaneStopInput extends ElectronLauncherWireGuardRuntimeOptions {
64
+ routePlan?: LauncherRoutePlan | null;
65
+ ownerId?: string | null;
66
+ ownershipStatePath?: string | null;
67
+ skipOwnershipState?: boolean | null;
68
+ }
69
+ export interface ElectronLauncherStandaloneDataPlaneStopResult {
70
+ ok: boolean;
71
+ wireGuard: Awaited<ReturnType<typeof stopLauncherWireGuardPeer>>;
72
+ message: string;
73
+ }
74
+ export interface ElectronLauncherStandaloneRouteProbe {
75
+ target: ElectronLauncherStandaloneDataPlaneProbeTarget;
76
+ label: string;
77
+ address: string;
78
+ required: boolean;
79
+ ok: boolean;
80
+ viaWireGuard: boolean;
81
+ viaLoopback: boolean;
82
+ viaProxyTun: boolean;
83
+ interfaceName: string | null;
84
+ gateway: string | null;
85
+ expectedInterfaceName: string | null;
86
+ error: string | null;
87
+ raw: string | null;
88
+ }
89
+ export interface ElectronLauncherStandaloneEndpointProbe {
90
+ endpoint: string;
91
+ host: string | null;
92
+ ok: boolean;
93
+ viaProxyTun: boolean;
94
+ interfaceName: string | null;
95
+ gateway: string | null;
96
+ error: string | null;
97
+ }
98
+ export interface ElectronLauncherStandaloneDataPlaneDiagnostics {
99
+ ok: boolean;
100
+ state: ElectronLauncherStandaloneDataPlaneState;
101
+ severity: ElectronLauncherStandaloneDataPlaneSeverity;
102
+ message: string;
103
+ productId: string | null;
104
+ leaseIp: string | null;
105
+ serviceVip: string | null;
106
+ dnsServer: string | null;
107
+ internalControlIp: string | null;
108
+ domesticGatewayIp: string | null;
109
+ probes: ElectronLauncherStandaloneRouteProbe[];
110
+ endpoint: ElectronLauncherStandaloneEndpointProbe | null;
111
+ updatedAt: string;
112
+ }
113
+ export declare function diagnoseElectronLauncherStandaloneDataPlane(input: ElectronLauncherStandaloneDataPlaneInput): ElectronLauncherStandaloneDataPlaneDiagnostics;
114
+ export declare function applyElectronLauncherStandaloneDataPlane(input: ElectronLauncherStandaloneDataPlaneApplyInput): Promise<ElectronLauncherStandaloneDataPlaneApplyResult>;
115
+ export declare function stopElectronLauncherStandaloneDataPlane(input: ElectronLauncherStandaloneDataPlaneStopInput): Promise<ElectronLauncherStandaloneDataPlaneStopResult>;
116
+ export declare function readElectronLauncherStandaloneOwnershipState(statePath?: string | null): ElectronLauncherStandaloneOwnershipState;
117
+ export declare function upsertElectronLauncherStandaloneOwnershipClaim(claim: ElectronLauncherNetworkOwnershipClaim, statePath?: string | null): ElectronLauncherStandaloneOwnershipState;
118
+ export declare function releaseElectronLauncherStandaloneOwnershipClaim(ownerId: string, statePath?: string | null): ElectronLauncherStandaloneOwnershipState;
119
+ export declare function buildElectronLauncherStandaloneOwnershipClaim(routePlan: LauncherRoutePlan, input?: ElectronLauncherStandaloneOwnershipInput): ElectronLauncherNetworkOwnershipClaim;
@@ -0,0 +1,441 @@
1
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, join } from 'node:path';
4
+ import { isIP } from 'node:net';
5
+ import { classifyLauncherIpv4Address } from './network-diagnostics.js';
6
+ import { buildElectronLauncherNetworkOwnershipRegistry } from './network-ownership-registry.js';
7
+ import { connectLauncherWireGuardPeer, getLauncherWireGuardPeerStatus, stopLauncherWireGuardPeer, probeLauncherWireGuardEndpoint, probeLauncherWireGuardRoute } from './wireguard.js';
8
+ export function diagnoseElectronLauncherStandaloneDataPlane(input) {
9
+ const routePlan = input.routePlan ?? null;
10
+ const leaseIp = ipv4OrNull(input.leaseIp) ?? ipv4OrNull(routePlan?.leaseIp);
11
+ const serviceVip = ipv4OrNull(input.serviceVip) ?? ipv4OrNull(routePlan?.serviceVip);
12
+ const dnsServer = dnsServerHost(input.dnsServer) ?? dnsServerHost(routePlan?.dnsServer);
13
+ const internalControlIp = ipv4OrNull(input.internalControlIp) ?? ipv4OrNull(routePlan?.internalControlIp);
14
+ const domesticGatewayIp = ipv4OrNull(input.domesticGatewayIp) ?? ipv4OrNull(routePlan?.domesticGatewayIp);
15
+ const endpoint = stringValue(input.endpoint)
16
+ ?? stringValue(routePlan?.domesticRelayEndpoint)
17
+ ?? stringValue(routePlan?.h2iDirectEndpoint);
18
+ if (!leaseIp && !routePlan) {
19
+ return buildDiagnostics({
20
+ state: 'lease-missing',
21
+ severity: 'info',
22
+ message: 'Launcher lease has not been issued yet.',
23
+ productId: null,
24
+ leaseIp,
25
+ serviceVip,
26
+ dnsServer,
27
+ internalControlIp,
28
+ domesticGatewayIp,
29
+ probes: [],
30
+ endpoint: null
31
+ });
32
+ }
33
+ const expectedInterfaceName = stringValue(input.expectedInterfaceName);
34
+ const expectedInterfaceAddresses = input.expectedInterfaceAddresses ?? (leaseIp ? [leaseIp] : []);
35
+ const requiredTargets = input.requiredProbeTargets
36
+ ? new Set(input.requiredProbeTargets)
37
+ : null;
38
+ const requiredProbe = (target, address) => Boolean(address) && (!requiredTargets || requiredTargets.has(target));
39
+ const probes = [
40
+ probeRouteTarget({
41
+ target: 'lease-ip',
42
+ label: 'Lease IP self route',
43
+ address: leaseIp,
44
+ required: requiredProbe('lease-ip', leaseIp)
45
+ }, expectedInterfaceName, expectedInterfaceAddresses),
46
+ probeRouteTarget({
47
+ target: 'internal-control',
48
+ label: 'Internal control plane',
49
+ address: internalControlIp,
50
+ required: requiredProbe('internal-control', internalControlIp)
51
+ }, expectedInterfaceName, expectedInterfaceAddresses),
52
+ probeRouteTarget({
53
+ target: 'domestic-gateway',
54
+ label: 'Domestic DNS relay',
55
+ address: domesticGatewayIp,
56
+ required: requiredProbe('domestic-gateway', domesticGatewayIp)
57
+ }, expectedInterfaceName, expectedInterfaceAddresses),
58
+ probeRouteTarget({
59
+ target: 'service-vip',
60
+ label: 'Product service VIP',
61
+ address: serviceVip,
62
+ required: requiredProbe('service-vip', serviceVip)
63
+ }, expectedInterfaceName, expectedInterfaceAddresses),
64
+ probeRouteTarget({
65
+ target: 'dns-server',
66
+ label: 'WireGuard DNS server',
67
+ address: dnsServer,
68
+ required: requiredProbe('dns-server', dnsServer)
69
+ }, expectedInterfaceName, expectedInterfaceAddresses)
70
+ ].filter((probe) => Boolean(probe));
71
+ const endpointProbe = endpoint ? normalizeEndpointProbe(probeLauncherWireGuardEndpoint({ endpoint })) : null;
72
+ const requiredProbes = probes.filter((probe) => probe.required);
73
+ const failedProbes = requiredProbes.filter((probe) => !probe.ok);
74
+ const proxyProbe = requiredProbes.find((probe) => probe.viaProxyTun) ?? null;
75
+ const endpointProxyCaptured = endpointProbe?.viaProxyTun === true;
76
+ const wireGuardActive = input.wireGuardActive === true;
77
+ const state = dataPlaneState({
78
+ leaseIp,
79
+ failedProbes,
80
+ proxyCaptured: Boolean(proxyProbe || endpointProxyCaptured),
81
+ wireGuardActive
82
+ });
83
+ return buildDiagnostics({
84
+ state,
85
+ severity: dataPlaneSeverity(state),
86
+ message: dataPlaneMessage({
87
+ state,
88
+ proxyProbe,
89
+ endpointProbe,
90
+ failedProbes
91
+ }),
92
+ productId: stringValue(routePlan?.productId),
93
+ leaseIp,
94
+ serviceVip,
95
+ dnsServer,
96
+ internalControlIp,
97
+ domesticGatewayIp,
98
+ probes,
99
+ endpoint: endpointProbe
100
+ });
101
+ }
102
+ export async function applyElectronLauncherStandaloneDataPlane(input) {
103
+ const ownershipClaim = buildElectronLauncherStandaloneOwnershipClaim(input.routePlan, input);
104
+ const stateClaims = input.skipOwnershipState === true ? [] : readStandaloneOwnershipClaims(input.ownershipStatePath);
105
+ const ownershipRegistry = buildElectronLauncherNetworkOwnershipRegistry([
106
+ ...stateClaims.filter((claim) => claim.ownerId !== ownershipClaim.ownerId),
107
+ ...(input.existingClaims ?? []),
108
+ ownershipClaim
109
+ ]);
110
+ if (ownershipRegistry.conflicts.length > 0 && input.failOnOwnershipConflicts !== false) {
111
+ const diagnostics = ownershipConflictDiagnostics(input.routePlan, ownershipRegistry);
112
+ return {
113
+ ok: false,
114
+ state: diagnostics.state,
115
+ diagnostics,
116
+ ownershipClaim,
117
+ ownershipRegistry,
118
+ wireGuard: null,
119
+ message: diagnostics.message
120
+ };
121
+ }
122
+ if (input.skipOwnershipState !== true) {
123
+ writeStandaloneOwnershipClaim(input.ownershipStatePath, ownershipClaim);
124
+ }
125
+ const wireGuard = await connectLauncherWireGuardPeer({
126
+ ...input,
127
+ action: input.action ?? 'restart',
128
+ dnsDomains: input.dnsDomains ?? [],
129
+ suppressWireGuardDns: input.suppressWireGuardDns,
130
+ pathPreference: input.pathPreference
131
+ });
132
+ const diagnostics = await waitForStandaloneDataPlaneDiagnostics(input, wireGuard);
133
+ const finalOwnershipClaim = {
134
+ ...ownershipClaim,
135
+ state: diagnostics.ok ? 'active' : 'connecting',
136
+ updatedAt: new Date().toISOString()
137
+ };
138
+ if (input.skipOwnershipState !== true) {
139
+ writeStandaloneOwnershipClaim(input.ownershipStatePath, finalOwnershipClaim);
140
+ }
141
+ const finalOwnershipRegistry = buildElectronLauncherNetworkOwnershipRegistry([
142
+ ...stateClaims.filter((claim) => claim.ownerId !== ownershipClaim.ownerId),
143
+ ...(input.existingClaims ?? []),
144
+ finalOwnershipClaim
145
+ ]);
146
+ return {
147
+ ok: wireGuard.ok === true && diagnostics.ok,
148
+ state: diagnostics.state,
149
+ diagnostics,
150
+ ownershipClaim: finalOwnershipClaim,
151
+ ownershipRegistry: finalOwnershipRegistry,
152
+ wireGuard,
153
+ message: diagnostics.ok ? 'Launcher standalone data plane is ready.' : diagnostics.message
154
+ };
155
+ }
156
+ export async function stopElectronLauncherStandaloneDataPlane(input) {
157
+ const wireGuard = await stopLauncherWireGuardPeer(input);
158
+ if (input.skipOwnershipState !== true && input.ownerId) {
159
+ releaseStandaloneOwnershipClaim(input.ownershipStatePath, input.ownerId);
160
+ }
161
+ return {
162
+ ok: wireGuard.ok === true,
163
+ wireGuard,
164
+ message: wireGuard.message ?? (wireGuard.ok ? 'Launcher standalone data plane stopped.' : 'Launcher standalone data plane stop failed.')
165
+ };
166
+ }
167
+ export function readElectronLauncherStandaloneOwnershipState(statePath) {
168
+ const file = statePath || defaultStandaloneOwnershipStatePath();
169
+ const claims = readStandaloneOwnershipClaims(file);
170
+ return {
171
+ statePath: file,
172
+ claims,
173
+ registry: buildElectronLauncherNetworkOwnershipRegistry(claims),
174
+ updatedAt: new Date().toISOString()
175
+ };
176
+ }
177
+ export function upsertElectronLauncherStandaloneOwnershipClaim(claim, statePath) {
178
+ writeStandaloneOwnershipClaim(statePath, claim);
179
+ return readElectronLauncherStandaloneOwnershipState(statePath);
180
+ }
181
+ export function releaseElectronLauncherStandaloneOwnershipClaim(ownerId, statePath) {
182
+ releaseStandaloneOwnershipClaim(statePath, ownerId);
183
+ return readElectronLauncherStandaloneOwnershipState(statePath);
184
+ }
185
+ export function buildElectronLauncherStandaloneOwnershipClaim(routePlan, input = {}) {
186
+ const productId = stringValue(input.productId) ?? stringValue(routePlan.productId) ?? 'launcher';
187
+ const instanceId = stringValue(input.instanceId) ?? stringValue(routePlan.leaseIp);
188
+ const ownerId = stringValue(input.ownerId) ?? `${productId}:${instanceId ?? 'local'}`;
189
+ const dnsHosts = uniqueStrings(input.dnsHosts ?? []);
190
+ const dnsZones = uniqueStrings(input.dnsZones ?? []);
191
+ const routeCidrs = uniqueStrings(input.routeCidrs ?? []);
192
+ const productRouteCidrs = routeCidrs.length
193
+ ? routeCidrs
194
+ : routePlan.leaseCidr
195
+ ? [routePlan.leaseCidr]
196
+ : uniqueStrings(routePlan.routeCidrs ?? []);
197
+ return {
198
+ ownerId,
199
+ productId,
200
+ instanceId,
201
+ displayName: stringValue(input.displayName) ?? productId,
202
+ state: 'connecting',
203
+ priority: normalizePriority(input.priority),
204
+ leaseIp: ipv4OrNull(routePlan.leaseIp),
205
+ gatewayIp: ipv4OrNull(routePlan.internalControlIp) ?? ipv4OrNull(routePlan.domesticGatewayIp),
206
+ dnsHosts,
207
+ dnsZones,
208
+ routeCidrs: productRouteCidrs,
209
+ reverseProxyRoutes: input.reverseProxyRoutes ?? [],
210
+ metadata: {
211
+ launcherMode: routePlan.launcherMode,
212
+ serviceVip: routePlan.serviceVip,
213
+ snapshotId: routePlan.snapshotId,
214
+ dnsServer: routePlan.dnsServer,
215
+ ...(input.metadata ?? {})
216
+ },
217
+ updatedAt: new Date().toISOString()
218
+ };
219
+ }
220
+ function probeRouteTarget(target, expectedInterfaceName, expectedInterfaceAddresses) {
221
+ const address = ipv4OrNull(target.address);
222
+ if (!address)
223
+ return null;
224
+ const route = probeLauncherWireGuardRoute({
225
+ targetIp: address,
226
+ expectedInterfaceName,
227
+ expectedInterfaceAddresses
228
+ });
229
+ const viaProxyTun = isProxyTunAddress(route.gateway);
230
+ const selfRouteOk = target.target === 'lease-ip' && route.viaLoopback && !viaProxyTun && !route.error;
231
+ return {
232
+ target: target.target,
233
+ label: target.label,
234
+ address,
235
+ required: target.required,
236
+ ok: selfRouteOk || (route.ok && !viaProxyTun && !route.error),
237
+ viaWireGuard: route.ok,
238
+ viaLoopback: route.viaLoopback,
239
+ viaProxyTun,
240
+ interfaceName: route.interfaceName,
241
+ gateway: route.gateway,
242
+ expectedInterfaceName: route.expectedInterfaceName,
243
+ error: route.error,
244
+ raw: route.raw
245
+ };
246
+ }
247
+ function normalizeEndpointProbe(probe) {
248
+ return {
249
+ endpoint: probe.endpoint ?? '',
250
+ host: probe.host,
251
+ ok: probe.ok,
252
+ viaProxyTun: probe.viaProxyTun,
253
+ interfaceName: probe.interfaceName,
254
+ gateway: probe.gateway,
255
+ error: probe.error
256
+ };
257
+ }
258
+ function dataPlaneState(input) {
259
+ if (!input.leaseIp)
260
+ return 'lease-missing';
261
+ if (input.proxyCaptured)
262
+ return 'proxy-tun-captured';
263
+ if (input.failedProbes.length === 0)
264
+ return 'network-ready';
265
+ return input.wireGuardActive ? 'route-mismatch' : 'data-plane-pending';
266
+ }
267
+ function dataPlaneSeverity(state) {
268
+ if (state === 'network-ready')
269
+ return 'ok';
270
+ if (state === 'lease-missing' || state === 'lease-active')
271
+ return 'info';
272
+ if (state === 'ownership-conflict' || state === 'proxy-tun-captured' || state === 'route-mismatch' || state === 'service-unreachable')
273
+ return 'error';
274
+ return 'warning';
275
+ }
276
+ function dataPlaneMessage(input) {
277
+ if (input.state === 'lease-missing')
278
+ return 'Launcher lease has not been issued yet.';
279
+ if (input.state === 'network-ready') {
280
+ return 'Launcher lease and local data-plane routes are both ready.';
281
+ }
282
+ if (input.state === 'ownership-conflict') {
283
+ return 'Another launcher owner already claims the same DNS, route, or reverse-proxy resource.';
284
+ }
285
+ if (input.state === 'service-unreachable') {
286
+ return 'Launcher route proof is ready, but the product service VIP is not reachable yet.';
287
+ }
288
+ if (input.state === 'proxy-tun-captured') {
289
+ if (input.proxyProbe) {
290
+ return `${input.proxyProbe.label} is captured by proxy TUN gateway ${input.proxyProbe.gateway ?? 'unknown'}; keep Launcher internal CIDRs on the WireGuard/direct route.`;
291
+ }
292
+ return `WireGuard endpoint is captured by proxy TUN gateway ${input.endpointProbe?.gateway ?? 'unknown'}; endpoint traffic must stay outside Clash/mihomo TUN.`;
293
+ }
294
+ const names = input.failedProbes.map((probe) => probe.label).join(', ');
295
+ if (input.state === 'route-mismatch') {
296
+ return `WireGuard is active but route proof is not ready: ${names || 'no required routes'}.`;
297
+ }
298
+ return `Launcher lease is active, but privileged WireGuard/data-plane apply is still pending: ${names || 'no route proof'}.`;
299
+ }
300
+ async function waitForStandaloneDataPlaneDiagnostics(input, wireGuard) {
301
+ const attempts = Math.max(1, Math.min(30, Math.floor(input.dataPlaneProbeAttempts ?? 12)));
302
+ const intervalMs = Math.max(250, Math.min(5000, Math.floor(input.dataPlaneProbeIntervalMs ?? 1000)));
303
+ let diagnostics = standaloneDiagnosticsForWireGuard(input, wireGuard);
304
+ for (let index = 1; index < attempts && !diagnostics.ok; index += 1) {
305
+ await delay(intervalMs);
306
+ const status = getLauncherWireGuardPeerStatus(input);
307
+ diagnostics = standaloneDiagnosticsForWireGuard(input, {
308
+ ...wireGuard,
309
+ ok: wireGuard.ok === true || status?.active === true,
310
+ status
311
+ });
312
+ }
313
+ return diagnostics;
314
+ }
315
+ function standaloneDiagnosticsForWireGuard(input, wireGuard) {
316
+ const status = objectRecord(wireGuard.status);
317
+ return diagnoseElectronLauncherStandaloneDataPlane({
318
+ routePlan: input.routePlan,
319
+ leaseIp: input.routePlan.leaseIp,
320
+ serviceVip: input.routePlan.serviceVip,
321
+ dnsServer: input.routePlan.dnsServer,
322
+ endpoint: wireGuard.peer?.endpoint,
323
+ requiredProbeTargets: input.requiredProbeTargets,
324
+ expectedInterfaceName: stringValue(status?.realInterfaceName) || stringValue(status?.interfaceName),
325
+ expectedInterfaceAddresses: Array.isArray(status?.addresses) ? status.addresses.filter((item) => typeof item === 'string') : [input.routePlan.leaseIp],
326
+ wireGuardActive: wireGuard.ok === true || status?.active === true
327
+ });
328
+ }
329
+ function ownershipConflictDiagnostics(routePlan, registry) {
330
+ const conflict = registry.conflicts[0];
331
+ return buildDiagnostics({
332
+ state: 'ownership-conflict',
333
+ severity: 'error',
334
+ message: conflict
335
+ ? `Launcher ownership conflict on ${conflict.resource}:${conflict.key} (${conflict.reason}).`
336
+ : 'Launcher ownership conflict detected.',
337
+ productId: stringValue(routePlan.productId),
338
+ leaseIp: ipv4OrNull(routePlan.leaseIp),
339
+ serviceVip: ipv4OrNull(routePlan.serviceVip),
340
+ dnsServer: dnsServerHost(routePlan.dnsServer),
341
+ internalControlIp: ipv4OrNull(routePlan.internalControlIp),
342
+ domesticGatewayIp: ipv4OrNull(routePlan.domesticGatewayIp),
343
+ probes: [],
344
+ endpoint: null
345
+ });
346
+ }
347
+ function defaultStandaloneOwnershipStatePath() {
348
+ if (process.platform === 'darwin') {
349
+ return join(homedir(), 'Library', 'Application Support', 'QPJoy', 'Electron Launcher', 'standalone-ownership.json');
350
+ }
351
+ if (process.platform === 'win32') {
352
+ return join(process.env.APPDATA || homedir(), 'QPJoy', 'Electron Launcher', 'standalone-ownership.json');
353
+ }
354
+ return join(process.env.XDG_STATE_HOME || join(homedir(), '.local', 'state'), 'qpjoy-electron-launcher', 'standalone-ownership.json');
355
+ }
356
+ function readStandaloneOwnershipClaims(statePath) {
357
+ try {
358
+ const parsed = JSON.parse(readFileSync(statePath || defaultStandaloneOwnershipStatePath(), 'utf8'));
359
+ return Array.isArray(parsed.claims)
360
+ ? parsed.claims.filter((claim) => Boolean(claim?.ownerId))
361
+ : [];
362
+ }
363
+ catch {
364
+ return [];
365
+ }
366
+ }
367
+ function writeStandaloneOwnershipClaim(statePath, claim) {
368
+ const file = statePath || defaultStandaloneOwnershipStatePath();
369
+ const claims = [
370
+ ...readStandaloneOwnershipClaims(file).filter((row) => row.ownerId !== claim.ownerId),
371
+ claim
372
+ ];
373
+ writeStandaloneOwnershipState(file, claims);
374
+ }
375
+ function releaseStandaloneOwnershipClaim(statePath, ownerId) {
376
+ const file = statePath || defaultStandaloneOwnershipStatePath();
377
+ const claims = readStandaloneOwnershipClaims(file).filter((row) => row.ownerId !== ownerId);
378
+ writeStandaloneOwnershipState(file, claims);
379
+ }
380
+ function writeStandaloneOwnershipState(file, claims) {
381
+ mkdirSync(dirname(file), { recursive: true });
382
+ writeFileSync(file, JSON.stringify({
383
+ version: 1,
384
+ claims,
385
+ updatedAt: new Date().toISOString()
386
+ }, null, 2), { mode: 0o600 });
387
+ }
388
+ function delay(ms) {
389
+ return new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
390
+ }
391
+ function buildDiagnostics(input) {
392
+ return {
393
+ ok: input.state === 'network-ready',
394
+ state: input.state,
395
+ severity: input.severity,
396
+ message: input.message,
397
+ productId: input.productId,
398
+ leaseIp: input.leaseIp,
399
+ serviceVip: input.serviceVip,
400
+ dnsServer: input.dnsServer,
401
+ internalControlIp: input.internalControlIp,
402
+ domesticGatewayIp: input.domesticGatewayIp,
403
+ probes: input.probes,
404
+ endpoint: input.endpoint,
405
+ updatedAt: new Date().toISOString()
406
+ };
407
+ }
408
+ function isProxyTunAddress(value) {
409
+ return Boolean(value && classifyLauncherIpv4Address(value) === 'proxy-fake-ip');
410
+ }
411
+ function normalizePriority(value) {
412
+ const number = typeof value === 'number' ? value : Number(value);
413
+ return Number.isFinite(number) ? Math.trunc(number) : 100;
414
+ }
415
+ function uniqueStrings(values) {
416
+ return values
417
+ .map((value) => String(value || '').trim())
418
+ .filter((value, index, rows) => Boolean(value) && rows.indexOf(value) === index);
419
+ }
420
+ function objectRecord(value) {
421
+ return value && typeof value === 'object' ? value : {};
422
+ }
423
+ function dnsServerHost(value) {
424
+ const clean = stringValue(value);
425
+ if (!clean)
426
+ return null;
427
+ const bracket = clean.match(/^\[([^\]]+)](?::\d{1,5})?$/);
428
+ if (bracket?.[1])
429
+ return ipv4OrNull(bracket[1]);
430
+ const ipv4WithPort = clean.match(/^(\d{1,3}(?:\.\d{1,3}){3}):\d{1,5}$/);
431
+ if (ipv4WithPort?.[1])
432
+ return ipv4OrNull(ipv4WithPort[1]);
433
+ return ipv4OrNull(clean);
434
+ }
435
+ function ipv4OrNull(value) {
436
+ const clean = stringValue(value);
437
+ return clean && isIP(clean) === 4 ? clean : null;
438
+ }
439
+ function stringValue(value) {
440
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
441
+ }
@@ -0,0 +1,91 @@
1
+ import { type ElectronLauncherNetworkOwnershipClaim, type ElectronLauncherNetworkOwnershipRegistry } from './network-ownership-registry.js';
2
+ export type ElectronLauncherPacMatchMode = 'direct' | 'proxy';
3
+ export type ElectronLauncherSystemResolverMode = 'off' | 'dynamic' | 'file';
4
+ export interface ElectronLauncherSystemDomainProxyPolicy {
5
+ enabled?: boolean;
6
+ domains?: string[] | null;
7
+ pacUrl?: string | null;
8
+ proxy?: string | null;
9
+ matchMode?: ElectronLauncherPacMatchMode | null;
10
+ fallbackProxy?: string | null;
11
+ pacPort?: number | null;
12
+ dnsServers?: string[] | null;
13
+ dnsFallbackTarget?: string | null;
14
+ systemResolver?: boolean | ElectronLauncherSystemResolverMode | null;
15
+ reverseProxyRoutes?: ElectronLauncherSystemDomainProxyRoute[] | null;
16
+ ownershipClaim?: ElectronLauncherNetworkOwnershipClaim | null;
17
+ }
18
+ export interface ElectronLauncherSystemDomainProxyOptions {
19
+ userDataDir: string;
20
+ statePath?: string;
21
+ pacPort?: number | null;
22
+ log?: Pick<Console, 'warn'> | null;
23
+ }
24
+ export interface ElectronLauncherSystemDomainProxyStatus {
25
+ supported: boolean;
26
+ applied: boolean;
27
+ platform: NodeJS.Platform;
28
+ pacUrl?: string | null;
29
+ proxy?: string | null;
30
+ matchMode?: ElectronLauncherPacMatchMode | null;
31
+ fallbackProxy?: string | null;
32
+ pacPort?: number | null;
33
+ sharedPac?: boolean;
34
+ dnsServers?: string[];
35
+ dnsFallbackTarget?: string | null;
36
+ systemResolver?: boolean;
37
+ systemResolverMode?: ElectronLauncherSystemResolverMode;
38
+ resolverDomains?: string[];
39
+ resolverPort?: number | null;
40
+ resolverApplied?: boolean;
41
+ resolverError?: string | null;
42
+ domains?: string[];
43
+ reverseProxyRoutes?: ElectronLauncherSystemDomainProxyRoute[];
44
+ ownershipRegistry?: ElectronLauncherNetworkOwnershipRegistry | null;
45
+ updatedAt?: string | null;
46
+ changed?: boolean;
47
+ verified?: boolean;
48
+ actual?: unknown;
49
+ reason?: string;
50
+ restored?: boolean;
51
+ skipped?: boolean;
52
+ staleState?: boolean;
53
+ orphanCleanup?: boolean;
54
+ pending?: boolean;
55
+ externalApply?: boolean;
56
+ darwinApplyShell?: string | null;
57
+ error?: string;
58
+ }
59
+ export interface ElectronLauncherPacProxy {
60
+ address: string;
61
+ directive: string;
62
+ }
63
+ export interface ElectronLauncherSystemDomainProxyRoute {
64
+ routeId?: string | null;
65
+ host: string;
66
+ dnsTarget?: string | null;
67
+ targetUrl?: string | null;
68
+ tlsMode?: 'internal' | 'passthrough' | 'edge-terminated' | null;
69
+ authRequired?: boolean | null;
70
+ enabled?: boolean | null;
71
+ }
72
+ export interface ElectronLauncherSystemDomainProxyManager {
73
+ apply(policy: ElectronLauncherSystemDomainProxyPolicy): Promise<ElectronLauncherSystemDomainProxyStatus>;
74
+ disable(reason?: string): Promise<ElectronLauncherSystemDomainProxyStatus>;
75
+ restoreStale(reason?: string): Promise<ElectronLauncherSystemDomainProxyStatus>;
76
+ darwinPrepareApply?(policy: ElectronLauncherSystemDomainProxyPolicy): Promise<ElectronLauncherSystemDomainProxyStatus>;
77
+ completeExternalApply?(reason?: string): Promise<ElectronLauncherSystemDomainProxyStatus>;
78
+ darwinRestoreScript?(): string | null;
79
+ completeExternalRestore?(reason?: string): Promise<ElectronLauncherSystemDomainProxyStatus>;
80
+ status(): ElectronLauncherSystemDomainProxyStatus;
81
+ statusVerified(): Promise<ElectronLauncherSystemDomainProxyStatus>;
82
+ close(): Promise<void>;
83
+ }
84
+ export declare function createElectronLauncherSystemDomainProxy(options: ElectronLauncherSystemDomainProxyOptions): ElectronLauncherSystemDomainProxyManager;
85
+ export declare function renderElectronLauncherPacScript(input: {
86
+ domains: string[];
87
+ proxy?: ElectronLauncherPacProxy | null;
88
+ matchMode?: ElectronLauncherPacMatchMode | null;
89
+ fallbackProxy?: ElectronLauncherPacProxy | null;
90
+ ownershipClaims?: ElectronLauncherNetworkOwnershipClaim[] | null;
91
+ }): string;