@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 +164 -0
- package/dist/index.d.ts +79 -0
- package/dist/index.js +62 -0
- package/dist/network-diagnostics.d.ts +42 -0
- package/dist/network-diagnostics.js +254 -0
- package/dist/network-ownership-registry.d.ts +68 -0
- package/dist/network-ownership-registry.js +286 -0
- package/dist/release-updater.d.ts +129 -0
- package/dist/release-updater.js +195 -0
- package/dist/standalone-data-plane.d.ts +119 -0
- package/dist/standalone-data-plane.js +441 -0
- package/dist/system-domain-proxy.d.ts +91 -0
- package/dist/system-domain-proxy.js +2729 -0
- package/dist/wireguard.d.ts +289 -0
- package/dist/wireguard.js +902 -0
- package/package.json +61 -0
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
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|