@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.
package/README.md ADDED
@@ -0,0 +1,164 @@
1
+ # @qpjoy/electron-launcher
2
+
3
+ Product-facing npm entry for MX Launcher.
4
+
5
+ Applications install this package directly and point it at an MX Launcher
6
+ backend. The package hides the internal split between core, embed, and
7
+ standalone adapters.
8
+
9
+ For production SDK delivery, MX Launcher also supports role-specific packages:
10
+
11
+ | Package | Use when |
12
+ | --- | --- |
13
+ | `@qpjoy/mx-launcher-core` | sharing protocol, manifest, capability, release, and ABI contracts |
14
+ | `@qpjoy/mx-launcher-standalone` | building a capability owner such as MX-H2I or Luopan |
15
+ | `@qpjoy/mx-launcher-embed-sdk` | building an embed app such as AppCenter, H2O, or an AppCenter-managed app |
16
+ | `@qpjoy/electron-launcher` | using the unified Electron facade or existing compatibility exports |
17
+
18
+ Both delivery styles are supported. The role-specific split is preferred for
19
+ external teams because it keeps owner-only APIs out of embed apps. The unified
20
+ Electron package remains useful for monorepo demos, legacy products, and apps
21
+ that want a single import surface. Packages with the same major version must be
22
+ protocol-compatible; breaking protocol or broker ABI changes require a new
23
+ major version.
24
+
25
+ See the package design notes in `../README.md`, `../launcher-core/README.md`,
26
+ `../launcher-standalone/README.md`, and `../launcher-embed-sdk/README.md`.
27
+
28
+ ```ts
29
+ import { createElectronLauncher, defineLauncherProduct } from '@qpjoy/electron-launcher';
30
+
31
+ export const product = defineLauncherProduct({
32
+ productId: 'h2o',
33
+ displayName: 'H2O',
34
+ mode: 'embed',
35
+ standaloneChannelProductId: 'mx-h2i',
36
+ launcherActions: {
37
+ network: true,
38
+ release: true,
39
+ update: true,
40
+ rollout: true,
41
+ appCenter: true
42
+ }
43
+ });
44
+
45
+ const launcher = createElectronLauncher({
46
+ baseUrl: 'http://127.0.0.1:18090',
47
+ productId: product.productId,
48
+ mode: product.mode,
49
+ standaloneChannelProductId: product.standaloneChannelProductId,
50
+ requiredCapabilities: ['user.session', 'network.proxy', 'network.status']
51
+ });
52
+
53
+ const connection = launcher.mode === 'embed'
54
+ ? await launcher.connect()
55
+ : null;
56
+
57
+ if (connection && !connection.ok) {
58
+ console.warn(connection.state, connection.message);
59
+ }
60
+ ```
61
+
62
+ `standalone` mode is for the full Launcher shell. `embed` mode is for product
63
+ apps hosted by a standalone broker session. Embed apps do not allocate their own
64
+ WireGuard peer lease or apply DNS/route state; call `launcher.connect()` and
65
+ request scoped capabilities from the selected standalone channel.
66
+
67
+ ## Runtime adapters
68
+
69
+ The package exposes optional runtime adapters as separate subpaths so products
70
+ can depend only on the capabilities they own:
71
+
72
+ | Subpath | Purpose |
73
+ | --- | --- |
74
+ | `@qpjoy/electron-launcher` | launcher client, product definition, standalone/embed SDK facade |
75
+ | `@qpjoy/electron-launcher/wireguard` | WireGuard profile rendering, route proof, and peer recovery |
76
+ | `@qpjoy/electron-launcher/system-domain-proxy` | standalone local PAC/proxy edge plus OS PAC apply/restore/verify |
77
+ | `@qpjoy/electron-launcher/standalone-data-plane` | read-only standalone lease/data-plane route diagnostics |
78
+
79
+ For standalone products, keep the state model split into two phases:
80
+
81
+ 1. `connectNetwork()` proves the control plane accepted the app and issued a
82
+ lease.
83
+ 2. WireGuard/PAC/DNS apply and route proof prove the local data plane is ready.
84
+
85
+ Products should not display `connected` only because a lease exists. Use the
86
+ read-only data-plane diagnostic before claiming traffic is ready:
87
+
88
+ ```ts
89
+ import {
90
+ applyElectronLauncherStandaloneDataPlane,
91
+ diagnoseElectronLauncherStandaloneDataPlane
92
+ } from '@qpjoy/electron-launcher/standalone-data-plane';
93
+
94
+ const dataPlane = diagnoseElectronLauncherStandaloneDataPlane({
95
+ routePlan: session.routePlan,
96
+ leaseIp: session.lease.leaseIp,
97
+ serviceVip: session.lease.serviceVip
98
+ });
99
+
100
+ if (!dataPlane.ok) {
101
+ console.warn(dataPlane.state, dataPlane.message);
102
+ }
103
+
104
+ const applied = await applyElectronLauncherStandaloneDataPlane({
105
+ userDataDir: app.getPath('userData'),
106
+ profileName: `${product.productId}.conf`,
107
+ routePlan: session.routePlan,
108
+ privateKey: session.wireGuard.privateKey,
109
+ dnsDomains: ['mxinfo-inc.cn'],
110
+ ownerId: `${product.productId}:${installId}`,
111
+ productId: product.productId,
112
+ instanceId: installId,
113
+ dnsHosts: ['luopan.mxinfo-inc.cn'],
114
+ dnsZones: ['mxinfo-inc.cn'],
115
+ failOnOwnershipConflicts: true
116
+ });
117
+ ```
118
+
119
+ Inside the `electron-dock/mx-launcher` monorepo, demos depend on this package
120
+ with `workspace:*`, so local package changes are used by MX-H2I, Luopan, and
121
+ future launcher demos during development. Release builds should consume a
122
+ published semver version from npm, matching the V1 HDO local-vs-published
123
+ workflow.
124
+
125
+ System PAC is intended for Launcher standalone owners. For MX-H2I the local edge
126
+ listens on `127.0.0.1:2053` by default and serves both `/proxy.pac` and an
127
+ HTTP/CONNECT proxy on TCP, plus a UDP DNS relay on the same port. Internal
128
+ domains are resolved with the Internal IP from the route plan, `10.88.88.88` by
129
+ default, through the WireGuard AllowedIPs path. The Domestic gateway
130
+ `10.88.0.1` is only a fallback relay/cache target; all other domains fall back
131
+ to the previous system proxy when one is detected or provided. If another
132
+ standalone launcher already owns the same local edge port, a later instance
133
+ reuses it.
134
+
135
+ On macOS, local PAC alone only covers proxy-aware traffic such as browsers. When
136
+ `systemResolver: 'dynamic'` is enabled, the package installs a runtime
137
+ SystemConfiguration supplemental DNS entry for matched domains and points it at
138
+ `127.0.0.1:<pacPort>`, so `ping`, CLI tools, and non-PAC applications resolve
139
+ the same Internal names without writing `/etc/hosts` or `/etc/resolver`. A file
140
+ resolver mode remains available as an explicit fallback and only manages files
141
+ with the `MX_ELECTRON_LAUNCHER_RESOLVER` marker.
142
+
143
+ ```ts
144
+ import { createElectronLauncherSystemDomainProxy } from '@qpjoy/electron-launcher/system-domain-proxy';
145
+
146
+ const systemDomainProxy = createElectronLauncherSystemDomainProxy({
147
+ userDataDir: app.getPath('userData'),
148
+ pacPort: 2053
149
+ });
150
+
151
+ await systemDomainProxy.apply({
152
+ enabled: true,
153
+ domains: ['internal.mx', 'svc.cluster.local'],
154
+ matchMode: 'proxy',
155
+ proxy: '127.0.0.1:2053',
156
+ pacPort: 2053,
157
+ systemResolver: 'dynamic',
158
+ dnsServers: ['10.88.88.88', '10.88.0.1'],
159
+ fallbackProxy: '127.0.0.1:7890'
160
+ });
161
+
162
+ // Later, when the launcher channel disconnects:
163
+ await systemDomainProxy.disable('disconnect');
164
+ ```
@@ -0,0 +1,79 @@
1
+ import { assertCompatibleBroker, brokerAbiVersion, createLauncherEmbedManifest, createLauncherClient, createLauncherNetworkSession, createLauncherWireGuardKeyPair, launcherBrokerCompatibility, launcherEmbedRuntimeContractVersion, launcherNetworkScopeForMode, launcherProtocolVersion, networkDigest, networkRefreshKey, normalizeLauncherBaseUrl, routePlanFromSnapshot, shouldRefreshNetwork, type FetchLike, type LauncherBrokerCapabilitySession, type LauncherBrokerHandshakeDeniedReason, type LauncherBrokerHandshakeRequest, type LauncherBrokerHandshakeResult, type LauncherCapability, type LauncherChannelRecord, type LauncherClient, type LauncherClientOptions, type LauncherEmbedConnectionState, type LauncherEmbedBrokerRequestName, type LauncherEmbedManifestInput, type LauncherEmbedRuntimeEventName, type LauncherIdentityKind, type LauncherManifest, type LauncherNetworkLease, type LauncherNetworkLeaseInput, type LauncherNetworkScope, type LauncherNetworkSession, type LauncherNetworkSessionInput, type LauncherNetworkSnapshot, type LauncherNetworkSnapshotInput, type LauncherProductMode, type LauncherProductNetwork, type LauncherProductNetworkInput, type LauncherProductUpdatePolicy, type LauncherRoutePlan, type LauncherWireGuardKeyMaterial, type LauncherWireGuardKeyPair, type LauncherWireGuardKeyProvider } from '@qpjoy/mx-launcher-core';
2
+ import { createEmbedLauncher, type EmbedConnectOptions, type EmbedConnectionResult, type EmbedBrokerConnectOptions, type EmbedBrokerRequestHandler, type EmbedLauncherConnection, type EmbedLauncherEventHandler, type EmbedLauncherEventName, type EmbedLauncher, type EmbedLauncherOptions, type EmbedLegacyNetworkApi, type LauncherChannelRegistrySource, type EmbedSnapshotOptions } from '@qpjoy/mx-launcher-embed-sdk';
3
+ import { createStandaloneLauncher, type StandaloneLeaseOptions, type StandaloneNetworkSessionOptions, type StandaloneConnectOptions, type StandaloneConnectionResult, type StandaloneLauncher, type StandaloneLauncherOptions, type StandaloneSnapshotOptions, type StandaloneUserConnectOptions } from '@qpjoy/mx-launcher-standalone';
4
+ export { createElectronLauncherSystemDomainProxy, renderElectronLauncherPacScript, type ElectronLauncherPacProxy, type ElectronLauncherPacMatchMode, type ElectronLauncherSystemDomainProxyManager, type ElectronLauncherSystemDomainProxyOptions, type ElectronLauncherSystemDomainProxyPolicy, type ElectronLauncherSystemDomainProxyRoute, type ElectronLauncherSystemDomainProxyStatus } from './system-domain-proxy.js';
5
+ export { classifyLauncherIpv4Address, diagnoseLauncherHostResolution, type ElectronLauncherAddressDiagnostic, type ElectronLauncherHostResolutionDiagnostics, type ElectronLauncherHostResolutionDiagnosticInput, type ElectronLauncherHostResolutionSeverity, type ElectronLauncherHostResolutionState, type ElectronLauncherIpClassification, type ElectronLauncherNetworkPhase } from './network-diagnostics.js';
6
+ export { buildElectronLauncherNetworkOwnershipRegistry, mergedElectronLauncherDnsZones, mergedElectronLauncherReverseProxyRoutes, resolveElectronLauncherDnsOwner, type ElectronLauncherNetworkOwner, type ElectronLauncherNetworkOwnerState, type ElectronLauncherNetworkOwnershipClaim, type ElectronLauncherNetworkOwnershipConflict, type ElectronLauncherNetworkOwnershipEntry, type ElectronLauncherNetworkOwnershipRegistry, type ElectronLauncherNetworkOwnershipResource, type ElectronLauncherNetworkOwnershipRoute } from './network-ownership-registry.js';
7
+ export { applyElectronLauncherStandaloneDataPlane, buildElectronLauncherStandaloneOwnershipClaim, diagnoseElectronLauncherStandaloneDataPlane, readElectronLauncherStandaloneOwnershipState, releaseElectronLauncherStandaloneOwnershipClaim, stopElectronLauncherStandaloneDataPlane, upsertElectronLauncherStandaloneOwnershipClaim, type ElectronLauncherStandaloneDataPlaneApplyInput, type ElectronLauncherStandaloneDataPlaneApplyResult, type ElectronLauncherStandaloneDataPlaneDiagnostics, type ElectronLauncherStandaloneDataPlaneInput, type ElectronLauncherStandaloneDataPlaneProbeTarget, type ElectronLauncherStandaloneDataPlaneSeverity, type ElectronLauncherStandaloneDataPlaneStopInput, type ElectronLauncherStandaloneDataPlaneStopResult, type ElectronLauncherStandaloneDataPlaneState, type ElectronLauncherStandaloneEndpointProbe, type ElectronLauncherStandaloneOwnershipInput, type ElectronLauncherStandaloneOwnershipState, type ElectronLauncherStandaloneRouteProbe } from './standalone-data-plane.js';
8
+ export { createElectronLauncherReleaseUpdater, downloadElectronLauncherReleaseArtifactToFile, type ElectronLauncherArtifactDownloadInput, type ElectronLauncherArtifactDownloadResult, type ElectronLauncherReleaseActivationMode, type ElectronLauncherReleaseArtifactRef, type ElectronLauncherReleasePlan, type ElectronLauncherReleasePolicyDecision, type ElectronLauncherReleaseReportInput, type ElectronLauncherReleaseUpdateMode, type ElectronLauncherReleaseUpdater, type ElectronLauncherReleaseUpdaterOptions, type ElectronLauncherUpdateCheckInput, type ElectronLauncherUpdateCheckResult } from './release-updater.js';
9
+ export declare const ELECTRON_LAUNCHER_PACKAGE_NAME = "@qpjoy/electron-launcher";
10
+ export type ElectronLauncherMode = LauncherProductMode;
11
+ export interface ElectronLauncherOptions extends LauncherClientOptions {
12
+ productId?: string;
13
+ appId?: string;
14
+ mode?: ElectronLauncherMode;
15
+ standaloneChannelProductId?: string;
16
+ requiredCapabilities?: LauncherCapability[];
17
+ sdkVersion?: string;
18
+ appVersion?: string;
19
+ displayName?: string;
20
+ description?: string;
21
+ packageName?: string;
22
+ category?: string;
23
+ runtimeContractVersion?: string;
24
+ channelRegistry?: LauncherChannelRegistrySource;
25
+ requestImpl?: EmbedBrokerRequestHandler;
26
+ installId?: string;
27
+ deviceId?: string;
28
+ siteId?: string;
29
+ publicKey?: string;
30
+ privateKey?: string;
31
+ keyPair?: LauncherWireGuardKeyPair;
32
+ keyProvider?: LauncherWireGuardKeyProvider;
33
+ deviceLabel?: string;
34
+ }
35
+ export type ElectronLauncher = EmbedLauncher | StandaloneLauncher;
36
+ export interface LauncherProductActions {
37
+ network: boolean;
38
+ release: boolean;
39
+ update: boolean;
40
+ rollout: boolean;
41
+ appCenter: boolean;
42
+ }
43
+ export interface LauncherProductDefinition {
44
+ productId: string;
45
+ displayName: string;
46
+ mode: ElectronLauncherMode;
47
+ networkScope: LauncherNetworkScope;
48
+ standaloneChannelProductId: string;
49
+ appCenter: {
50
+ visible: boolean;
51
+ category: string;
52
+ };
53
+ release: {
54
+ componentId: string;
55
+ channel: string;
56
+ rolloutGroup: string | null;
57
+ };
58
+ launcherActions: LauncherProductActions;
59
+ }
60
+ export interface LauncherProductDefinitionInput {
61
+ productId: string;
62
+ displayName?: string;
63
+ mode?: ElectronLauncherMode;
64
+ standaloneChannelProductId?: string;
65
+ appCenter?: {
66
+ visible?: boolean;
67
+ category?: string;
68
+ };
69
+ release?: {
70
+ componentId?: string;
71
+ channel?: string;
72
+ rolloutGroup?: string | null;
73
+ };
74
+ launcherActions?: Partial<LauncherProductActions>;
75
+ }
76
+ export declare function createElectronLauncher(options: ElectronLauncherOptions): ElectronLauncher;
77
+ export declare function defineLauncherProduct(input: LauncherProductDefinitionInput): LauncherProductDefinition;
78
+ export { assertCompatibleBroker, brokerAbiVersion, createEmbedLauncher, createLauncherClient, createLauncherEmbedManifest, createLauncherNetworkSession, createLauncherWireGuardKeyPair, createStandaloneLauncher, launcherBrokerCompatibility, launcherEmbedRuntimeContractVersion, launcherNetworkScopeForMode, launcherProtocolVersion, networkDigest, networkRefreshKey, normalizeLauncherBaseUrl, routePlanFromSnapshot, shouldRefreshNetwork };
79
+ export type { EmbedBrokerConnectOptions, EmbedBrokerRequestHandler, EmbedConnectOptions, EmbedConnectionResult, EmbedLauncherConnection, EmbedLauncherEventHandler, EmbedLauncherEventName, EmbedLauncher, EmbedLauncherOptions, EmbedLegacyNetworkApi, EmbedSnapshotOptions, FetchLike, LauncherBrokerCapabilitySession, LauncherBrokerHandshakeDeniedReason, LauncherBrokerHandshakeRequest, LauncherBrokerHandshakeResult, LauncherCapability, LauncherChannelRegistrySource, LauncherChannelRecord, LauncherClient, LauncherClientOptions, LauncherEmbedConnectionState, LauncherEmbedBrokerRequestName, LauncherEmbedManifestInput, LauncherEmbedRuntimeEventName, LauncherIdentityKind, LauncherManifest, LauncherNetworkLease, LauncherNetworkLeaseInput, LauncherNetworkScope, LauncherNetworkSession, LauncherNetworkSessionInput, LauncherNetworkSnapshot, LauncherNetworkSnapshotInput, LauncherProductMode, LauncherProductNetwork, LauncherProductNetworkInput, LauncherProductUpdatePolicy, LauncherRoutePlan, LauncherWireGuardKeyMaterial, LauncherWireGuardKeyPair, LauncherWireGuardKeyProvider, StandaloneConnectOptions, StandaloneConnectionResult, StandaloneLeaseOptions, StandaloneNetworkSessionOptions, StandaloneLauncher, StandaloneLauncherOptions, StandaloneSnapshotOptions, StandaloneUserConnectOptions };
package/dist/index.js ADDED
@@ -0,0 +1,62 @@
1
+ import { assertCompatibleBroker, brokerAbiVersion, createLauncherEmbedManifest, createLauncherClient, createLauncherNetworkSession, createLauncherWireGuardKeyPair, launcherBrokerCompatibility, launcherEmbedRuntimeContractVersion, launcherNetworkScopeForMode, launcherProtocolVersion, networkDigest, networkRefreshKey, normalizeLauncherBaseUrl, routePlanFromSnapshot, shouldRefreshNetwork } from '@qpjoy/mx-launcher-core';
2
+ import { createEmbedLauncher } from '@qpjoy/mx-launcher-embed-sdk';
3
+ import { createStandaloneLauncher } from '@qpjoy/mx-launcher-standalone';
4
+ export { createElectronLauncherSystemDomainProxy, renderElectronLauncherPacScript } from './system-domain-proxy.js';
5
+ export { classifyLauncherIpv4Address, diagnoseLauncherHostResolution } from './network-diagnostics.js';
6
+ export { buildElectronLauncherNetworkOwnershipRegistry, mergedElectronLauncherDnsZones, mergedElectronLauncherReverseProxyRoutes, resolveElectronLauncherDnsOwner } from './network-ownership-registry.js';
7
+ export { applyElectronLauncherStandaloneDataPlane, buildElectronLauncherStandaloneOwnershipClaim, diagnoseElectronLauncherStandaloneDataPlane, readElectronLauncherStandaloneOwnershipState, releaseElectronLauncherStandaloneOwnershipClaim, stopElectronLauncherStandaloneDataPlane, upsertElectronLauncherStandaloneOwnershipClaim } from './standalone-data-plane.js';
8
+ export { createElectronLauncherReleaseUpdater, downloadElectronLauncherReleaseArtifactToFile } from './release-updater.js';
9
+ export const ELECTRON_LAUNCHER_PACKAGE_NAME = '@qpjoy/electron-launcher';
10
+ export function createElectronLauncher(options) {
11
+ const mode = options.mode ?? 'embed';
12
+ if (mode === 'standalone') {
13
+ return createStandaloneLauncher({
14
+ ...options,
15
+ productId: options.productId ?? 'launcher'
16
+ });
17
+ }
18
+ return createEmbedLauncher({
19
+ ...options,
20
+ productId: requiredProductId(options.productId ?? options.appId),
21
+ appId: options.appId ?? options.productId
22
+ });
23
+ }
24
+ export function defineLauncherProduct(input) {
25
+ const productId = requiredProductId(input.productId);
26
+ const displayName = input.displayName?.trim() || productId;
27
+ const mode = input.mode ?? 'embed';
28
+ const standaloneChannelProductId = mode === 'standalone'
29
+ ? productId
30
+ : input.standaloneChannelProductId?.trim() || 'mx-h2i';
31
+ const actions = input.launcherActions ?? {};
32
+ return {
33
+ productId,
34
+ displayName,
35
+ mode,
36
+ networkScope: launcherNetworkScopeForMode(mode),
37
+ standaloneChannelProductId,
38
+ appCenter: {
39
+ visible: input.appCenter?.visible ?? true,
40
+ category: input.appCenter?.category?.trim() || 'app'
41
+ },
42
+ release: {
43
+ componentId: input.release?.componentId?.trim() || productId,
44
+ channel: input.release?.channel?.trim() || 'stable',
45
+ rolloutGroup: input.release?.rolloutGroup ?? null
46
+ },
47
+ launcherActions: {
48
+ network: actions.network ?? true,
49
+ release: actions.release ?? true,
50
+ update: actions.update ?? true,
51
+ rollout: actions.rollout ?? true,
52
+ appCenter: actions.appCenter ?? true
53
+ }
54
+ };
55
+ }
56
+ function requiredProductId(productId) {
57
+ const trimmed = productId?.trim();
58
+ if (!trimmed)
59
+ throw new Error('Electron Launcher productId is required for embed mode');
60
+ return trimmed;
61
+ }
62
+ export { assertCompatibleBroker, brokerAbiVersion, createEmbedLauncher, createLauncherClient, createLauncherEmbedManifest, createLauncherNetworkSession, createLauncherWireGuardKeyPair, createStandaloneLauncher, launcherBrokerCompatibility, launcherEmbedRuntimeContractVersion, launcherNetworkScopeForMode, launcherProtocolVersion, networkDigest, networkRefreshKey, normalizeLauncherBaseUrl, routePlanFromSnapshot, shouldRefreshNetwork };
@@ -0,0 +1,42 @@
1
+ export type ElectronLauncherNetworkPhase = 'bootstrap' | 'connected' | 'disconnected' | 'unknown';
2
+ export type ElectronLauncherIpClassification = 'expected-internal-target' | 'internal-overlay' | 'v1-hdo-overlay' | 'proxy-fake-ip' | 'loopback' | 'private' | 'public' | 'invalid';
3
+ export type ElectronLauncherHostResolutionState = 'expected-internal' | 'internal-overlay' | 'v1-hdo-overlay' | 'proxy-fake-ip' | 'public' | 'private' | 'loopback' | 'unresolved' | 'invalid-host';
4
+ export type ElectronLauncherHostResolutionSeverity = 'ok' | 'info' | 'warning' | 'error';
5
+ export interface ElectronLauncherAddressDiagnostic {
6
+ address: string;
7
+ classification: ElectronLauncherIpClassification;
8
+ }
9
+ export interface ElectronLauncherHostResolutionDiagnostics {
10
+ host: string | null;
11
+ phase: ElectronLauncherNetworkPhase;
12
+ addresses: ElectronLauncherAddressDiagnostic[];
13
+ state: ElectronLauncherHostResolutionState;
14
+ severity: ElectronLauncherHostResolutionSeverity;
15
+ ok: boolean;
16
+ message: string;
17
+ expectedInternalTargets: string[];
18
+ internalCidrs: string[];
19
+ v1HdoCidrs: string[];
20
+ proxyFakeIpCidrs: string[];
21
+ error?: string | null;
22
+ updatedAt: string;
23
+ }
24
+ export interface ElectronLauncherHostResolutionDiagnosticInput {
25
+ host?: string | null;
26
+ phase?: ElectronLauncherNetworkPhase | null;
27
+ expectedInternalTargets?: Array<string | null | undefined> | null;
28
+ internalCidrs?: Array<string | null | undefined> | null;
29
+ v1HdoCidrs?: Array<string | null | undefined> | null;
30
+ proxyFakeIpCidrs?: Array<string | null | undefined> | null;
31
+ lookup?: ((host: string) => Promise<Array<string | {
32
+ address?: string | null;
33
+ family?: number | null;
34
+ }>>) | null;
35
+ }
36
+ export declare function diagnoseLauncherHostResolution(input: ElectronLauncherHostResolutionDiagnosticInput): Promise<ElectronLauncherHostResolutionDiagnostics>;
37
+ export declare function classifyLauncherIpv4Address(address: string, options?: {
38
+ expectedInternalTargets?: Array<string | null | undefined> | null;
39
+ internalCidrs?: Array<string | null | undefined> | null;
40
+ v1HdoCidrs?: Array<string | null | undefined> | null;
41
+ proxyFakeIpCidrs?: Array<string | null | undefined> | null;
42
+ }): ElectronLauncherIpClassification;
@@ -0,0 +1,254 @@
1
+ import { lookup as dnsLookup } from 'node:dns/promises';
2
+ import { isIP } from 'node:net';
3
+ const DEFAULT_INTERNAL_CIDRS = ['10.88.0.0/13'];
4
+ const DEFAULT_V1_HDO_CIDRS = ['100.88.0.0/16', '100.89.0.0/16', '100.90.0.0/16', '100.91.0.0/16'];
5
+ const DEFAULT_PROXY_FAKE_IP_CIDRS = ['198.18.0.0/15'];
6
+ const PRIVATE_CIDRS = [
7
+ '10.0.0.0/8',
8
+ '172.16.0.0/12',
9
+ '192.168.0.0/16',
10
+ '100.64.0.0/10',
11
+ '169.254.0.0/16'
12
+ ];
13
+ const LOOPBACK_CIDRS = ['127.0.0.0/8'];
14
+ export async function diagnoseLauncherHostResolution(input) {
15
+ const host = normalizeHost(input.host);
16
+ const phase = normalizePhase(input.phase);
17
+ const expectedInternalTargets = normalizeIpv4List(input.expectedInternalTargets);
18
+ const internalCidrs = normalizeCidrList(input.internalCidrs, DEFAULT_INTERNAL_CIDRS);
19
+ const v1HdoCidrs = normalizeCidrList(input.v1HdoCidrs, DEFAULT_V1_HDO_CIDRS);
20
+ const proxyFakeIpCidrs = normalizeCidrList(input.proxyFakeIpCidrs, DEFAULT_PROXY_FAKE_IP_CIDRS);
21
+ if (!host) {
22
+ return resolutionResult({
23
+ host: null,
24
+ phase,
25
+ addresses: [],
26
+ state: 'invalid-host',
27
+ expectedInternalTargets,
28
+ internalCidrs,
29
+ v1HdoCidrs,
30
+ proxyFakeIpCidrs,
31
+ error: 'host is empty'
32
+ });
33
+ }
34
+ try {
35
+ const records = isIP(host) === 4
36
+ ? [host]
37
+ : await resolveHostAddresses(host, input.lookup);
38
+ const addresses = uniqueStrings(records)
39
+ .filter((address) => isIP(address) === 4)
40
+ .map((address) => ({
41
+ address,
42
+ classification: classifyLauncherIpv4Address(address, {
43
+ expectedInternalTargets,
44
+ internalCidrs,
45
+ v1HdoCidrs,
46
+ proxyFakeIpCidrs
47
+ })
48
+ }));
49
+ return resolutionResult({
50
+ host,
51
+ phase,
52
+ addresses,
53
+ state: resolutionState(addresses),
54
+ expectedInternalTargets,
55
+ internalCidrs,
56
+ v1HdoCidrs,
57
+ proxyFakeIpCidrs
58
+ });
59
+ }
60
+ catch (err) {
61
+ return resolutionResult({
62
+ host,
63
+ phase,
64
+ addresses: [],
65
+ state: 'unresolved',
66
+ expectedInternalTargets,
67
+ internalCidrs,
68
+ v1HdoCidrs,
69
+ proxyFakeIpCidrs,
70
+ error: errorMessage(err)
71
+ });
72
+ }
73
+ }
74
+ export function classifyLauncherIpv4Address(address, options = {}) {
75
+ if (isIP(address) !== 4)
76
+ return 'invalid';
77
+ const expectedInternalTargets = normalizeIpv4List(options.expectedInternalTargets);
78
+ const internalCidrs = normalizeCidrList(options.internalCidrs, DEFAULT_INTERNAL_CIDRS);
79
+ const v1HdoCidrs = normalizeCidrList(options.v1HdoCidrs, DEFAULT_V1_HDO_CIDRS);
80
+ const proxyFakeIpCidrs = normalizeCidrList(options.proxyFakeIpCidrs, DEFAULT_PROXY_FAKE_IP_CIDRS);
81
+ if (expectedInternalTargets.includes(address))
82
+ return 'expected-internal-target';
83
+ if (cidrsContainIp(proxyFakeIpCidrs, address))
84
+ return 'proxy-fake-ip';
85
+ if (cidrsContainIp(LOOPBACK_CIDRS, address))
86
+ return 'loopback';
87
+ if (cidrsContainIp(v1HdoCidrs, address))
88
+ return 'v1-hdo-overlay';
89
+ if (cidrsContainIp(internalCidrs, address))
90
+ return 'internal-overlay';
91
+ if (cidrsContainIp(PRIVATE_CIDRS, address))
92
+ return 'private';
93
+ return 'public';
94
+ }
95
+ function resolutionResult(input) {
96
+ const { severity, ok, message } = evaluateResolution(input);
97
+ return {
98
+ host: input.host,
99
+ phase: input.phase,
100
+ addresses: input.addresses,
101
+ state: input.state,
102
+ severity,
103
+ ok,
104
+ message,
105
+ expectedInternalTargets: input.expectedInternalTargets,
106
+ internalCidrs: input.internalCidrs,
107
+ v1HdoCidrs: input.v1HdoCidrs,
108
+ proxyFakeIpCidrs: input.proxyFakeIpCidrs,
109
+ error: input.error ?? null,
110
+ updatedAt: new Date().toISOString()
111
+ };
112
+ }
113
+ function evaluateResolution(input) {
114
+ const host = input.host || 'host';
115
+ if (input.state === 'invalid-host') {
116
+ return { severity: 'warning', ok: false, message: '未配置可诊断的 bootstrap 域名。' };
117
+ }
118
+ if (input.state === 'unresolved') {
119
+ const suffix = input.error ? `:${input.error}` : '。';
120
+ if (input.phase === 'connected') {
121
+ return { severity: 'error', ok: false, message: `${host} 在 V2 connected 状态下未解析${suffix}` };
122
+ }
123
+ return { severity: 'warning', ok: false, message: `${host} 当前未解析${suffix}` };
124
+ }
125
+ if (input.phase === 'connected') {
126
+ if (input.state === 'expected-internal' || input.state === 'internal-overlay') {
127
+ return { severity: 'ok', ok: true, message: `${host} 已由 V2 split DNS 指向 Internal。` };
128
+ }
129
+ if (input.state === 'proxy-fake-ip') {
130
+ return { severity: 'error', ok: false, message: `${host} 仍命中 Clash/mihomo fake-ip;V2 split DNS 尚未接管该域名。` };
131
+ }
132
+ if (input.state === 'public') {
133
+ return { severity: 'error', ok: false, message: `${host} 仍走外部 DNS;V2 split DNS 尚未接管该域名。` };
134
+ }
135
+ if (input.state === 'v1-hdo-overlay') {
136
+ return { severity: 'warning', ok: false, message: `${host} 当前落在 V1 HDO 网段;V2 connected 状态应切到 10.* Internal。` };
137
+ }
138
+ return { severity: 'warning', ok: false, message: `${host} 当前解析到 ${input.state},不是 V2 Internal 目标。` };
139
+ }
140
+ if (input.phase === 'bootstrap') {
141
+ if (input.state === 'proxy-fake-ip') {
142
+ return { severity: 'info', ok: true, message: `${host} bootstrap 当前由系统代理/TUN fake-ip 接管,这只能作为外部可达路径,不是 V2 ready 证明。` };
143
+ }
144
+ if (input.state === 'public') {
145
+ return { severity: 'ok', ok: true, message: `${host} bootstrap 当前走外部 DNS,可用于连接前发现 Domestic/Internal 入口。` };
146
+ }
147
+ if (input.state === 'expected-internal' || input.state === 'internal-overlay') {
148
+ return { severity: 'info', ok: true, message: `${host} bootstrap 当前解析到 Internal;这通常依赖 V1 DNS 或保留 overlay。` };
149
+ }
150
+ return { severity: 'info', ok: true, message: `${host} bootstrap 当前解析到 ${input.state}。` };
151
+ }
152
+ if (input.phase === 'disconnected') {
153
+ if (input.state === 'public' || input.state === 'proxy-fake-ip') {
154
+ return { severity: 'ok', ok: true, message: `${host} disconnected 状态已回到系统/外部解析路径。` };
155
+ }
156
+ return { severity: 'warning', ok: true, message: `${host} disconnected 状态仍解析到 ${input.state};如果未开启 V1/V2/代理,可能存在残留 resolver 或 DNS cache。` };
157
+ }
158
+ return { severity: 'info', ok: true, message: `${host} 当前解析状态为 ${input.state}。` };
159
+ }
160
+ async function resolveHostAddresses(host, customLookup) {
161
+ const records = customLookup
162
+ ? await customLookup(host)
163
+ : await dnsLookup(host, { all: true, family: 4 });
164
+ return records
165
+ .map((record) => typeof record === 'string' ? record : record.address || '')
166
+ .map((address) => address.trim())
167
+ .filter(Boolean);
168
+ }
169
+ function resolutionState(addresses) {
170
+ if (addresses.length === 0)
171
+ return 'unresolved';
172
+ const classes = addresses.map((row) => row.classification);
173
+ if (classes.includes('expected-internal-target'))
174
+ return 'expected-internal';
175
+ if (classes.includes('internal-overlay'))
176
+ return 'internal-overlay';
177
+ if (classes.includes('v1-hdo-overlay'))
178
+ return 'v1-hdo-overlay';
179
+ if (classes.includes('proxy-fake-ip'))
180
+ return 'proxy-fake-ip';
181
+ if (classes.includes('public'))
182
+ return 'public';
183
+ if (classes.includes('private'))
184
+ return 'private';
185
+ if (classes.includes('loopback'))
186
+ return 'loopback';
187
+ return 'unresolved';
188
+ }
189
+ function normalizePhase(value) {
190
+ if (value === 'bootstrap' || value === 'connected' || value === 'disconnected')
191
+ return value;
192
+ return 'unknown';
193
+ }
194
+ function normalizeHost(value) {
195
+ const text = typeof value === 'string' ? value.trim() : '';
196
+ if (!text)
197
+ return null;
198
+ if (/^https?:\/\//i.test(text)) {
199
+ try {
200
+ return new URL(text).hostname.toLowerCase() || null;
201
+ }
202
+ catch {
203
+ return null;
204
+ }
205
+ }
206
+ return text.replace(/:\d{1,5}$/, '').toLowerCase();
207
+ }
208
+ function normalizeIpv4List(values) {
209
+ return uniqueStrings((values ?? [])
210
+ .map((value) => String(value || '').trim())
211
+ .filter((value) => isIP(value) === 4));
212
+ }
213
+ function normalizeCidrList(values, fallback) {
214
+ const normalized = (values?.length ? values : fallback)
215
+ .map((value) => String(value || '').trim())
216
+ .filter((value) => parseCidr(value) !== null);
217
+ return uniqueStrings(normalized);
218
+ }
219
+ function cidrsContainIp(cidrs, ip) {
220
+ return cidrs.some((cidr) => cidrContainsIp(cidr, ip));
221
+ }
222
+ function cidrContainsIp(cidr, ip) {
223
+ const parsed = parseCidr(cidr);
224
+ const target = ipv4ToInt(ip);
225
+ if (!parsed || target === null)
226
+ return false;
227
+ const mask = parsed.prefix === 0 ? 0 : (0xffffffff << (32 - parsed.prefix)) >>> 0;
228
+ return (target & mask) === (parsed.base & mask);
229
+ }
230
+ function parseCidr(value) {
231
+ const [ip, prefixText] = value.split('/');
232
+ const base = ipv4ToInt(ip);
233
+ const prefix = Number(prefixText);
234
+ if (base === null || !Number.isInteger(prefix) || prefix < 0 || prefix > 32)
235
+ return null;
236
+ return { base, prefix };
237
+ }
238
+ function ipv4ToInt(value) {
239
+ const text = value?.trim();
240
+ if (!text || isIP(text) !== 4)
241
+ return null;
242
+ const parts = text.split('.').map((part) => Number(part));
243
+ if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255))
244
+ return null;
245
+ return (((parts[0] * 256 + parts[1]) * 256 + parts[2]) * 256 + parts[3]) >>> 0;
246
+ }
247
+ function uniqueStrings(values) {
248
+ return values.filter((value, index, rows) => value && rows.indexOf(value) === index);
249
+ }
250
+ function errorMessage(err) {
251
+ if (err instanceof Error)
252
+ return err.message || String(err);
253
+ return String(err);
254
+ }