Files
clash-party/src/shared/types.d.ts
ezequielnick a092eb50c3 feat(plugin): CPX v2 availability hardening (squash of cpx-v2-availability-hardening)
Client-side hardening of the CPX airport plugin, squashed from 46 commits
forked at 89c2bb0e. Behaviour changes:

- Unified operation model (§0.4/0.5): one deadline + one AbortSignal + one
  persistence commit per operation; per-plugin lock → vault lock hierarchy;
  tombstone on delete; every wait (lock, DNS preflight, proxy resolution,
  vault decrypt) is bounded by the same budget.
- Routing: direct/proxy auto-fallback with a pre-send guard; proxied https
  builds its own CONNECT tunnel (an aborted hung CONNECT closes its socket);
  invalid local-proxy ports are refused instead of falling back to :80; the
  core's inbound credentials are carried to the local proxy; NAT64 and
  site-local IPv6 ranges are non-public.
- Gateways: multi-gateway recovery with one rediscovery per operation,
  normalized endpoint paths, signed discovery documents (Ed25519, seq/digest
  accept/align/rollback/equivocation), commit order vault → plugin.yaml.
- Subscriptions: a fetched subscription is validated by the core (mihomo -t)
  against the current override set before it replaces the profile, inside
  the profile write critical section (profile.yaml and override.yaml share
  one write queue); schedule fields are read at write time; the first
  subscription is activated through the real switch flow; profile deletion
  removes the record last so any failure stays retryable.
- Devices: a re-login that replaces a still-valid device records it in the
  vault (staleDevices) and retires it after the login, after later
  successful fetches and on removal; enroll compensation restores the old
  vault and keeps an un-revoked new device for retirement.
- Vault: on Linux the vault is persisted only behind a system secret store
  (backend name + ciphertext-prefix canary); otherwise it stays in memory.
  A cache-miss read releases the caller at the budget while the lock is held
  until the decrypt ends.
- Config caches (plugin.yaml, profile.yaml, override.yaml) can no longer be
  rolled back by a late cold read.
- Reference gateway and provider guides updated (deploy contract, discoveryUrls
  same-origin rule, /revoke after re-login); https-proxy-agent dropped.

Reviewed in a Codex loop (gpt-6, 38 calls): 74 findings, 68 fixed and
verified, 6 invalid, no backlog. Tests: vitest 501, gateway 106.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 22:44:13 +08:00

734 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

type OutboundMode = 'rule' | 'global' | 'direct'
type LogLevel = 'info' | 'debug' | 'warning' | 'error' | 'silent'
type SysProxyMode = 'auto' | 'manual'
type CardStatus = 'col-span-2' | 'col-span-1' | 'hidden'
type SiderCardKey =
| 'sysproxy'
| 'tun'
| 'profile'
| 'proxy'
| 'rule'
| 'resource'
| 'override'
| 'connection'
| 'mihomo'
| 'dns'
| 'sniff'
| 'log'
| 'substore'
| 'network'
| 'usage'
type NetworkInfoCardKey = 'ip' | 'topology' | 'latency'
type AppTheme = 'system' | 'light' | 'dark'
type MihomoGroupType = 'Selector' | 'URLTest' | 'Fallback' | 'LoadBalance' | 'Relay'
type Priority =
| 'PRIORITY_LOW'
| 'PRIORITY_BELOW_NORMAL'
| 'PRIORITY_NORMAL'
| 'PRIORITY_ABOVE_NORMAL'
| 'PRIORITY_HIGH'
| 'PRIORITY_HIGHEST'
type MihomoProxyType =
| 'Direct'
| 'Reject'
| 'RejectDrop'
| 'Pass'
| 'Dns'
| 'Compatible'
| 'Socks5'
| 'Http'
| 'Ssh'
| 'Shadowsocks'
| 'ShadowsocksR'
| 'Snell'
| 'Vmess'
| 'Vless'
| 'Trojan'
| 'Hysteria'
| 'Hysteria2'
| 'Tuic'
| 'WireGuard'
| 'Mieru'
| 'AnyTLS'
| 'Sudoku'
| 'Masque'
| 'TrustTunnel'
type TunStack = 'gvisor' | 'mixed' | 'system'
type FindProcessMode = 'off' | 'strict' | 'always'
type DnsMode = 'normal' | 'fake-ip' | 'redir-host' | 'hosts'
type FilterMode = 'blacklist' | 'whitelist' | 'rule'
type NetworkInterfaceInfo = os.NetworkInterfaceInfo
interface IAppVersion {
version: string
changelog: string
}
interface IMihomoVersion {
version: string
meta: boolean
}
interface IMihomoTrafficInfo {
up: number
down: number
}
interface IMihomoMemoryInfo {
inuse: number
oslimit: number
}
interface IMihomoLogInfo {
type: LogLevel
payload: string
time?: string
}
interface IMihomoRulesInfo {
rules: IMihomoRulesDetail[]
}
interface IMihomoRulesDetail {
type: string
payload: string
proxy: string
size: number
index: number
extra?: {
disabled: boolean
hitCount: number
hitAt: string
missCount: number
missAt: string
}
}
interface IMihomoConnectionsInfo {
downloadTotal: number
uploadTotal: number
connections?: IMihomoConnectionDetail[]
memory: number
}
interface IMihomoConnectionDetail {
id: string
isActive: boolean
metadata: {
network: 'tcp' | 'udp'
type: string
sourceIP: string
sourceGeoIP: string[]
sourceIPASN: string
destinationIP: string
destinationGeoIP: string[]
destinationIPASN: string
sourcePort: string
destinationPort: string
inboundIP: string
inboundPort: string
inboundName: string
inboundUser: string
host: string
dnsMode: string
uid: number
process: string
processPath: string
specialProxy: string
specialRules: string
remoteDestination: string
dscp: number
sniffHost: string
}
uploadSpeed?: number
downloadSpeed?: number
upload: number
download: number
start: string
chains: string[]
providerChains: string[]
rule: string
rulePayload: string
}
interface IMihomoHistory {
time: string
delay: number
}
type IMihomoGroupDelay = Record<string, number>
interface IMihomoDelay {
delay?: number
message?: string
}
interface IMihomoProxy {
alive: boolean
extra: Record<string, { alive: boolean; history: IMihomoHistory[] }>
history: IMihomoHistory[]
id: string
name: string
tfo: boolean
type: MihomoProxyType
udp: boolean
uot: boolean
xudp: boolean
mptcp: boolean
smux: boolean
interface?: string
'routing-mark'?: number
'provider-name'?: string
'dialer-proxy'?: string
}
interface IMihomoGroup {
alive: boolean
all: string[]
extra: Record<string, { alive: boolean; history: IMihomoHistory[] }>
testUrl?: string
expectedStatus?: string
fixed?: string
hidden: boolean
history: IMihomoHistory[]
icon: string
name: string
now: string
tfo: boolean
type: MihomoGroupType
udp: boolean
xudp: boolean
}
interface IMihomoProxies {
proxies: Record<string, IMihomoProxy | IMihomoGroup>
}
interface IMihomoMixedGroup extends IMihomoGroup {
all: (IMihomoProxy | IMihomoGroup)[]
}
interface IMihomoRuleProviders {
providers: Record<string, IMihomoRuleProvider>
}
interface IMihomoRuleProvider {
behavior: string
format: string
name: string
ruleCount: number
type: string
updatedAt: string
vehicleType: string
payload?: string[]
}
interface IMihomoProxyProviders {
providers: Record<string, IMihomoProxyProvider>
}
interface ISubscriptionUserInfoUpper {
Upload: number
Download: number
Total: number
Expire: number
}
interface IMihomoProxyProvider {
name: string
type: string
proxies?: IMihomoProxy[]
subscriptionInfo?: ISubscriptionUserInfoUpper
expectedStatus: string
testUrl?: string
updatedAt?: string
vehicleType: string
}
interface ISysProxyConfig {
enable: boolean
host?: string
mode?: SysProxyMode
bypass?: string[]
pacScript?: string
}
interface INetworkLatencyTarget {
name: string
url: string
}
interface ICustomTrayIcons {
off?: string
sysProxy?: string
tun?: string
}
interface IAppConfig {
core: 'mihomo' | 'mihomo-alpha' | 'mihomo-smart' | 'mihomo-specific'
specificVersion?: string
enableSmartCore: boolean
enableSmartOverride: boolean
smartCoreUseLightGBM: boolean
smartCoreCollectData: boolean
smartCoreStrategy: 'sticky-sessions' | 'round-robin'
smartCollectorSize?: number
proxyDisplayMode: 'simple' | 'full'
proxyDisplayOrder: 'default' | 'delay' | 'name'
profileDisplayDate?: 'expire' | 'update'
envType?: ('bash' | 'cmd' | 'powershell' | 'fish' | 'nushell')[]
proxyCols: 'auto' | '1' | '2' | '3' | '4'
hideUnavailableProxies?: boolean
connectionDirection: 'asc' | 'desc'
connectionOrderBy: 'time' | 'upload' | 'download' | 'uploadSpeed' | 'downloadSpeed'
connectionViewMode?: 'list' | 'table'
connectionTableColumns?: string[]
connectionTableColumnWidths?: Record<string, number>
connectionTableSortColumn?: string
connectionTableSortDirection?: 'asc' | 'desc'
displayIcon?: boolean
displayAppName?: boolean
spinFloatingIcon?: boolean
disableTray?: boolean
swapTrayClick?: boolean
showFloatingWindow?: boolean
floatingWindowCompatMode?: boolean
disableHardwareAcceleration?: boolean
connectionCardStatus?: CardStatus
dnsCardStatus?: CardStatus
logCardStatus?: CardStatus
hideConnectionCardWave?: boolean
pauseSSID?: string[]
disableDnsOnPauseSSID?: boolean
controlDnsBeforePause?: boolean
ssidProfileMap?: Record<string, string>
ssidProfileRestore?: boolean
mihomoCoreCardStatus?: CardStatus
overrideCardStatus?: CardStatus
profileCardStatus?: CardStatus
proxyCardStatus?: CardStatus
networkCardStatus?: CardStatus
resourceCardStatus?: CardStatus
ruleCardStatus?: CardStatus
sniffCardStatus?: CardStatus
substoreCardStatus?: CardStatus
sysproxyCardStatus?: CardStatus
tunCardStatus?: CardStatus
usageCardStatus?: CardStatus
githubToken?: string
gistAgeEncrypt?: boolean
gistAgeRecipient?: string
gistAgeSecretKey?: string
useSubStore: boolean
subStoreHost?: string
subStoreBackendSyncCron?: string
subStoreBackendDownloadCron?: string
subStoreBackendUploadCron?: string
autoQuitWithoutCore?: boolean
autoQuitWithoutCoreDelay?: number
autoQuitWithoutCoreMode?: 'core' | 'tray'
useCustomSubStore?: boolean
useProxyInSubStore?: boolean
pluginUseProxy?: boolean // 新装插件默认路由模式true → proxy安全保证降级false → auto
mihomoCpuPriority?: Priority
coreStartupMode?: 'log' | 'post-up'
customSubStoreUrl?: string
diffWorkDir?: boolean
autoSetDNS?: boolean
originDNS?: string
useWindowFrame: boolean
proxyInTray: boolean
showCurrentProxyInTray: boolean
enableTrafficLogger?: boolean
siderOrder: string[]
lastSelectedSiderCard?: SiderCardKey
rememberSelectedSiderCard?: boolean
lockSiderCards?: boolean
siderWidth: number
appTheme: AppTheme
customTheme?: string
autoCheckUpdate: boolean
autoUpdateProfileOnStart: boolean
silentUpdate: boolean
githubProxy?: string
silentStart: boolean
autoCloseConnection: boolean
sysProxy: ISysProxyConfig
maxLogDays: number
maxLogFileSize: number
disableAppLog?: boolean
disableCoreLog?: boolean
userAgent?: string
delayTestConcurrency?: number
delayTestUrl?: string
delayTestTimeout?: number
networkLatencyTargets?: INetworkLatencyTarget[]
networkIPProvider?: 'ip.sb' | 'ipwho.is' | 'ipapi.is'
networkInfoCardOrder?: NetworkInfoCardKey[]
subscriptionTimeout?: number
encryptedPassword?: number[]
controlDns?: boolean
controlSniff?: boolean
useDockIcon?: boolean
showTraffic?: boolean
disableTrayIconColor?: boolean
customTrayIcon?: string
customTrayIcons?: ICustomTrayIcons
trayProxyGroupStyle?: 'default' | 'submenu'
disableAnimations?: boolean
webdavUrl?: string
webdavDir?: string
webdavUsername?: string
webdavPassword?: string
webdavMaxBackups?: number
webdavBackupCron?: string
webdavIgnoreCert?: boolean
useNameserverPolicy: boolean
nameserverPolicy: { [key: string]: string | string[] }
showWindowShortcut?: string
showFloatingWindowShortcut?: string
triggerSysProxyShortcut?: string
triggerTunShortcut?: string
ruleModeShortcut?: string
globalModeShortcut?: string
directModeShortcut?: string
restartAppShortcut?: string
quitWithoutCoreShortcut?: string
copyEnvShortcut?: string
language?: 'zh-CN' | 'zh-TW' | 'en-US' | 'ru-RU' | 'fa-IR'
triggerMainWindowBehavior?: 'show' | 'toggle'
showMixedPort?: number
enableMixedPort?: boolean
showSocksPort?: number
enableSocksPort?: boolean
showHttpPort?: number
enableHttpPort?: boolean
showRedirPort?: number
enableRedirPort?: boolean
showTproxyPort?: number
enableTproxyPort?: boolean
testProfileOnStart?: boolean
useHotReloadProfile?: boolean
hotReloadProfileAutoCloseConnection?: boolean
}
interface IMihomoTunConfig {
enable?: boolean
stack?: TunStack
'auto-route'?: boolean
'auto-redirect'?: boolean
'auto-detect-interface'?: boolean
'dns-hijack'?: string[]
device?: string
mtu?: number
'strict-route'?: boolean
gso?: boolean
'gso-max-size'?: number
'udp-timeout'?: number
'iproute2-table-index'?: number
'iproute2-rule-index'?: number
'endpoint-independent-nat'?: boolean
'route-address-set'?: string[]
'route-exclude-address-set'?: string[]
'route-address'?: string[]
'route-exclude-address'?: string[]
'include-interface'?: string[]
'exclude-interface'?: string[]
'include-uid'?: number[]
'include-uid-range'?: string[]
'exclude-uid'?: number[]
'exclude-uid-range'?: string[]
'include-android-user'?: string[]
'include-package'?: string[]
'exclude-package'?: string[]
}
interface IMihomoDNSConfig {
enable?: boolean
listen?: string
ipv6?: boolean
'ipv6-timeout'?: number
'prefer-h3'?: boolean
'enhanced-mode'?: DnsMode
'fake-ip-range'?: string
'fake-ip-filter'?: string[]
'fake-ip-filter-mode'?: FilterMode
'use-hosts'?: boolean
'use-system-hosts'?: boolean
'respect-rules'?: boolean
'default-nameserver'?: string[]
nameserver?: string[]
fallback?: string[]
'fallback-filter'?: { [key: string]: boolean | string | string[] }
'proxy-server-nameserver'?: string[]
'direct-nameserver'?: string[]
'direct-nameserver-follow-policy'?: boolean
'nameserver-policy'?: { [key: string]: string | string[] }
'cache-algorithm'?: string
}
interface IMihomoSnifferConfig {
enable?: boolean
'parse-pure-ip'?: boolean
'override-destination'?: boolean
'force-dns-mapping'?: boolean
'force-domain'?: string[]
'skip-domain'?: string[]
'skip-dst-address'?: string[]
'skip-src-address'?: string[]
sniff?: {
HTTP?: {
ports: (number | string)[]
'override-destination'?: boolean
}
TLS?: {
ports: (number | string)[]
}
QUIC?: {
ports: (number | string)[]
}
}
}
interface IMihomoProfileConfig {
'store-selected'?: boolean
'store-fake-ip'?: boolean
}
interface IMihomoConfig {
'external-controller-pipe': string
'external-controller-unix': string
'external-controller': string
'external-ui': string
'external-ui-url': string
'external-controller-cors'?: {
'allow-origins'?: string[]
'allow-private-network'?: boolean
}
secret?: string
ipv6: boolean
mode: OutboundMode
'mixed-port': number
'allow-lan': boolean
'unified-delay': boolean
'tcp-concurrent': boolean
'log-level': LogLevel
'find-process-mode': FindProcessMode
'socks-port'?: number
'redir-port'?: number
'tproxy-port'?: number
'skip-auth-prefixes'?: string[]
'bind-address'?: string
'lan-allowed-ips'?: string[]
'lan-disallowed-ips'?: string[]
authentication: string[]
port?: number
proxies?: []
'proxy-groups'?: []
rules?: []
hosts?: { [key: string]: string | string[] }
'geodata-mode'?: boolean
'geo-auto-update'?: boolean
'geo-update-interval'?: number
'geox-url'?: {
geoip?: string
geosite?: string
mmdb?: string
asn?: string
}
tun: IMihomoTunConfig
dns: IMihomoDNSConfig
sniffer: IMihomoSnifferConfig
profile: IMihomoProfileConfig
}
interface IProfileConfig {
current?: string
items: IProfileItem[]
}
interface IOverrideItem {
id: string
type: 'remote' | 'local'
ext: 'js' | 'yaml'
name: string
updated: number
global?: boolean
url?: string
file?: string
}
interface IOverrideConfig {
items: IOverrideItem[]
}
interface ISubscriptionUserInfo {
upload: number
download: number
total: number
expire: number
}
interface IProfileItem {
id: string
type: 'remote' | 'local' | 'plugin'
name: string
url?: string // remote
file?: string // local
interval?: number | string
home?: string
updated?: number
override?: string[]
useProxy?: boolean
extra?: ISubscriptionUserInfo
substore?: boolean
allowFixedInterval?: boolean
autoUpdate?: boolean
authToken?: string
userAgent?: string
ageSecretKey?: string
updateTimeout?: number
pluginId?: string
}
interface ISubStoreSub {
name: string
displayName?: string
icon?: string
tag?: string[]
}
interface IPluginProvider {
name: string
icon?: string
site?: string
description?: string // §4 机场静态说明≤500 码点,已清洗
}
// .cpx v2 — public, unencrypted descriptor. Contains NO secrets.
interface IPluginDescriptor {
magic: 'CPXF'
v: 2
spec: 'cpx-plugin/2'
loginUrl: string // OAuth authorize endpoint, https, no query/fragment
provider: IPluginProvider
discoveryUrls?: string[] // §3 备用发现源:公网 https origin1..8,去重,不含 loginUrl 的 origin
providerPubKey?: string // §5 Ed25519 原始 32 字节公钥,标准 base64 带 padding每个 .cpx 谱系独立密钥
}
// Subset returned by previewPlugin for the install-confirm page (no records, no network)
interface IPluginDescriptorPreview {
name: string
icon?: string
site?: string
loginUrl: string // full url; UI shows the host
spec: string
discoveryHosts?: string[] // §3 备用发现域名(纯文本 host
description?: string // §4
}
// 发现结果§3/§5来自任一发现源的归一化候选。seq 与 digest 成对出现(仅签名文档)。
interface IDiscoveryCandidate {
gateways: string[]
endpoints: IGatewayEndpoints
seq?: number
digest?: string
loginUrl?: string
discoveryUrls?: string[]
}
// 签名发现文档的 payload§5.2
interface IDiscoveryPayload {
spec: 'cpx-plugin/2'
seq: number // 1 ≤ seq ≤ 2^531
gateways: string[]
endpoints: IGatewayEndpoints
loginUrl?: string
discoveryUrls?: string[] // 缺失 = 不改;[] = 清空
}
// 有 providerPubKey 的插件在发现时携带minSeq / currentDigest 来自 plugin.yaml
interface DiscoverySigner {
pubKeyB64: string
minSeq?: number
currentDigest?: string
}
interface IPluginFilePayload {
name: string
fileBytesB64: string
}
interface IGatewayEndpoints {
enroll: string
challenge: string
config: string
revoke: string
}
// /.well-known/cpx-gateway discovery response归一化后。线格式仍带 `gateway`= gateways[0])供旧客户端读取。
interface IGatewayWellKnown {
spec: 'cpx-plugin/2'
gateways: string[] // https origins, no path/query/fragment; 1..3, deduplicated
endpoints: IGatewayEndpoints
}
type IPluginStatus = 'needs-login' | 'active' | 'needs-reauth'
// 路由模式auto = 直连优先、失败回退代理§1direct / proxy 为用户显式覆盖,不回退。
type IPluginRouteMode = 'auto' | 'direct' | 'proxy'
interface IPluginItem {
id: string
name: string
icon?: string
site?: string
loginUrl: string // public metadata; required to re-open the browser after restart
spec: string
profileId?: string // absent while 'needs-login'; present once 'active'/'needs-reauth'
status: IPluginStatus
interval?: number
autoUpdate?: boolean
useProxy?: boolean // 过渡期镜像写routeMode === 'proxy';供降级到旧版本读取
routeMode?: IPluginRouteMode // §1缺失时按 useProxy / 全局 pluginUseProxy 推导
lastGoodRoute?: 'direct' | 'proxy' // §1仅 auto 模式读写,不是秘密
lastUpdateErrorReason?: 'blocked' | 'network' | 'server' // §4.2;客户端侧枚举,不含 host/IP
discoveryUrls?: string[] // §3 公开元数据,与 loginUrl 同级的静态信任根
description?: string // §4 机场静态说明
lastProviderMessage?: string // §4 上次失败时机场返回的 message成功后清空
providerPubKey?: string // §5 公开元数据
discoverySeq?: number // §5 提交标记,与 discoveryDigest 成对(同时存在或同时缺失)
discoveryDigest?: string // §5 SHA-256(payloadBytes) hex
created: number
updated: number
lastUpdateErrorType?: 'auth' | 'transient'
lastUpdateErrorAt?: number
nextRetryAt?: number
failureCount?: number
}
interface IPluginConfig {
items: IPluginItem[]
}
// 缓存的网关状态§2.3)。写出时始终镜像 gateway.gateway = lastGood ?? gateways[0],供降级到旧版本读取。
interface IPluginGatewayState {
gateway: string // 过渡期镜像:= lastGood ?? gateways[0]
gateways: string[] // 归一化后1..3
endpoints: IGatewayEndpoints
lastGood?: string // 必须 ∈ gateways否则视为未设置
}
// safeStorage-encrypted vault payload — the ONLY place secrets live.
interface IPluginVault {
devicePrivKey: string // Ed25519 raw 32-byte seed, base64 (standard, padded)
deviceId: string // UUIDv4
gateway: IPluginGatewayState
// 被新设备替换、但尚未在服务端回收的旧设备:重新登录成功后 best-effort 回收,失败留在这里等下次拉取 / 删除
staleDevices?: IPluginStaleDevice[]
}
interface IPluginStaleDevice {
deviceId: string
devicePrivKey: string
}