mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/mihomo-party-org/clash-party.git
synced 2026-09-20 08:03:39 +08:00
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>
734 lines
18 KiB
TypeScript
734 lines
18 KiB
TypeScript
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 origin,1..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^53−1
|
||
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 = 直连优先、失败回退代理(§1);direct / 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
|
||
}
|