huaweicloud-devkit 1.1.7 → 1.1.8-next.1
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 +1 -1
- package/README.zh-CN.md +1 -1
- package/package.json +2 -1
- package/plugins/huaweicloud-core/.claude-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.codex-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.cursor-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.hermes-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.workbuddy-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/hooks/huaweicloud-safety.py +19 -1
- package/plugins/huaweicloud-core/openclaw.plugin.json +1 -1
- package/plugins/huaweicloud-core/safety/policy.json +1 -1
- package/plugins/huaweicloud-core/safety/rules/cloud-risk-rules.json +57 -2
- package/plugins/huaweicloud-core/skills/huawei-billing/SKILL.md +19 -16
- package/plugins/huaweicloud-core/skills/huawei-modelarts/SKILL.md +1 -1
- package/plugins/huaweicloud-core/skills/huawei-sandbox/SKILL.md +9 -3
- package/plugins/huaweicloud-core/skills/huaweicloud-capability-discovery/SKILL.md +1 -1
- package/plugins/huaweicloud-core/skills/huaweicloud-cli-and-auth/SKILL.md +15 -2
- package/plugins/huaweicloud-core/src/auth/service.mjs +36 -0
- package/plugins/huaweicloud-core/src/data/icons-manifest.v1.json +23 -5
- package/plugins/huaweicloud-core/src/detect-framework.mjs +41 -5
- package/plugins/huaweicloud-core/src/hcloud-cli.mjs +78 -8
- package/plugins/huaweicloud-core/src/hcloud-probe.mjs +10 -2
- package/plugins/huaweicloud-core/src/mcp-server.mjs +9 -0
- package/plugins/huaweicloud-core/src/preflight.mjs +125 -0
- package/plugins/huaweicloud-core/src/proxy/proxy-config.mjs +94 -1
- package/plugins/huaweicloud-core/src/risk-rule-engine.mjs +1 -1
- package/plugins/huaweicloud-core/src/safety-policy.mjs +165 -33
- package/plugins/huaweicloud-core/src/sandbox/session-manager.mjs +214 -27
- package/plugins/huaweicloud-core/src/search-market.mjs +40 -1
- package/plugins/huaweicloud-core/src/setup-cli.mjs +57 -17
- package/plugins/huaweicloud-core/src/tools.mjs +307 -20
- package/rules/huawei-agent-rules.mdc +78 -0
|
@@ -6,7 +6,7 @@ import { dirname, join } from 'node:path';
|
|
|
6
6
|
|
|
7
7
|
import { classifyHcloudArgs, redactSecrets, assertAllowed } from './safety-policy.mjs';
|
|
8
8
|
import { getProxySettings } from './proxy/proxy-config.mjs';
|
|
9
|
-
import {
|
|
9
|
+
import { resolveHcloudCommand } from './hcloud-probe.mjs';
|
|
10
10
|
import { parseStsExpiry, resolveCredentialsWithRuntime } from './auth/credentials.mjs';
|
|
11
11
|
|
|
12
12
|
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
@@ -69,6 +69,17 @@ function pruneStale(map, now = Date.now()) {
|
|
|
69
69
|
return changed;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
// Approval token lifecycle states, surfaced to callers so they can emit the
|
|
73
|
+
// precise JSON contract required by D4-24 (#745):
|
|
74
|
+
// 'valid' — token exists, not expired, not yet consumed
|
|
75
|
+
// 'expired' — token exists but createdAt is past APPROVAL_TTL_MS
|
|
76
|
+
// 'already_consumed' — token was consumed on a prior call (tombstone kept)
|
|
77
|
+
// 'not_found' — token was never created (unknown / typo / pruned)
|
|
78
|
+
const TOKEN_STATE_VALID = 'valid';
|
|
79
|
+
const TOKEN_STATE_EXPIRED = 'expired';
|
|
80
|
+
const TOKEN_STATE_ALREADY_CONSUMED = 'already_consumed';
|
|
81
|
+
const TOKEN_STATE_NOT_FOUND = 'not_found';
|
|
82
|
+
|
|
72
83
|
export function createApprovalToken(rawArgs) {
|
|
73
84
|
const token = randomUUID();
|
|
74
85
|
const map = readApprovals();
|
|
@@ -82,16 +93,36 @@ export function createApprovalToken(rawArgs) {
|
|
|
82
93
|
return token;
|
|
83
94
|
}
|
|
84
95
|
|
|
96
|
+
// Inspect a token's lifecycle state WITHOUT consuming it. Used by
|
|
97
|
+
// runApprovedCommand to decide between the 'expired' / 'already_consumed'
|
|
98
|
+
// / 'not_found' JSON shapes. A consumed token leaves a tombstone
|
|
99
|
+
// ({ consumedAt }) until it ages out past APPROVAL_TTL_MS.
|
|
100
|
+
export function inspectApprovalToken(token) {
|
|
101
|
+
const map = readApprovals();
|
|
102
|
+
const entry = map[token];
|
|
103
|
+
if (!entry) return { state: TOKEN_STATE_NOT_FOUND };
|
|
104
|
+
if (entry.consumedAt !== undefined) return { state: TOKEN_STATE_ALREADY_CONSUMED };
|
|
105
|
+
if (Date.now() - entry.createdAt > APPROVAL_TTL_MS) return { state: TOKEN_STATE_EXPIRED };
|
|
106
|
+
return { state: TOKEN_STATE_VALID, entry };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Backward-compatible single-use consume. Returns the stored entry on first
|
|
110
|
+
// call, then null. After consumption a tombstone ({ consumedAt }) is written
|
|
111
|
+
// so a second call can be distinguished from an unknown token via
|
|
112
|
+
// inspectApprovalToken. Kept for existing tests that assert null on reuse.
|
|
85
113
|
export function consumeApprovalToken(token) {
|
|
86
114
|
const map = readApprovals();
|
|
87
115
|
const entry = map[token];
|
|
88
116
|
if (!entry) return null;
|
|
117
|
+
if (entry.consumedAt !== undefined) return null;
|
|
89
118
|
if (Date.now() - entry.createdAt > APPROVAL_TTL_MS) {
|
|
90
119
|
delete map[token];
|
|
91
120
|
writeApprovals(map);
|
|
92
121
|
return null;
|
|
93
122
|
}
|
|
94
|
-
|
|
123
|
+
// Mark as consumed (tombstone) instead of deleting outright, so a repeat
|
|
124
|
+
// call can be classified as already_processed rather than not_found.
|
|
125
|
+
map[token] = { consumedAt: Date.now(), createdAt: entry.createdAt };
|
|
95
126
|
writeApprovals(map);
|
|
96
127
|
return entry;
|
|
97
128
|
}
|
|
@@ -391,6 +422,24 @@ function appendLangHint(result, service, metaDir) {
|
|
|
391
422
|
};
|
|
392
423
|
}
|
|
393
424
|
|
|
425
|
+
// Returns true when the current credentials are a TEMPORARY STS set that has
|
|
426
|
+
// already expired (per HW_STS_EXPIRES_AT / token expiry). Lets us fail fast with
|
|
427
|
+
// a clear CREDENTIAL_EXPIRED instead of letting a doomed IAM call return a
|
|
428
|
+
// misleading error. Permanent AK/SK (no token) and unparseable expiry are never
|
|
429
|
+
// treated as expired.
|
|
430
|
+
function isTemporaryStsExpired() {
|
|
431
|
+
let creds;
|
|
432
|
+
try {
|
|
433
|
+
creds = resolveCredentialsWithRuntime({ allowMissing: true });
|
|
434
|
+
} catch {
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
if (!creds?.securityToken) return false;
|
|
438
|
+
const expiry = parseStsExpiry({ securityToken: creds.securityToken });
|
|
439
|
+
if (expiry === null) return false; // unknown → don't block
|
|
440
|
+
return expiry <= Date.now();
|
|
441
|
+
}
|
|
442
|
+
|
|
394
443
|
export async function runHcloud(args, options = {}) {
|
|
395
444
|
const normalizedArgs = Array.isArray(args) ? args.map(String) : [];
|
|
396
445
|
const plan = {
|
|
@@ -399,6 +448,21 @@ export async function runHcloud(args, options = {}) {
|
|
|
399
448
|
};
|
|
400
449
|
assertAllowed(plan.classification);
|
|
401
450
|
|
|
451
|
+
// A: fail fast when the platform-injected temporary STS is already expired —
|
|
452
|
+
// otherwise hcloud would make a doomed IAM round trip and surface a misleading
|
|
453
|
+
// error (e.g. "缺少 cli-domain-id"). Permanent AK/SK are unaffected.
|
|
454
|
+
if (isTemporaryStsExpired()) {
|
|
455
|
+
return {
|
|
456
|
+
ok: false,
|
|
457
|
+
code: 'CREDENTIAL_EXPIRED',
|
|
458
|
+
error:
|
|
459
|
+
'Temporary STS credentials have expired. Re-login or start a new session so the platform injects fresh credentials, then retry.',
|
|
460
|
+
stderr: '',
|
|
461
|
+
stdout: '',
|
|
462
|
+
plan,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
402
466
|
// Link B: when the runtime/environment carries live temporary STS credentials
|
|
403
467
|
// (security token set), KooCLI/obsutil would not see them (they read S2/S3 only
|
|
404
468
|
// and R3 forbids writing them to disk). Inject per-command so hcloud actually
|
|
@@ -430,6 +494,8 @@ async function runHcloudOnceWithRetries(plan, options) {
|
|
|
430
494
|
if (result.ok || attempt >= maxRetries || !isRetryableNetworkError(result)) {
|
|
431
495
|
const merged = {
|
|
432
496
|
...result,
|
|
497
|
+
ok: result.ok ?? false,
|
|
498
|
+
exitCode: result.exitCode ?? null,
|
|
433
499
|
retries: attempt,
|
|
434
500
|
attempts: attempt + 1,
|
|
435
501
|
};
|
|
@@ -460,15 +526,15 @@ async function runtimeCurrentMismatchWarning() {
|
|
|
460
526
|
}
|
|
461
527
|
}
|
|
462
528
|
|
|
463
|
-
function discoverHcloudPath() {
|
|
464
|
-
return findHcloudBin();
|
|
465
|
-
}
|
|
466
|
-
|
|
467
529
|
function runHcloudOnce(plan, options) {
|
|
468
530
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
469
531
|
const forceKillAfterMs = options.forceKillAfterMs ?? DEFAULT_FORCE_KILL_AFTER_MS;
|
|
470
|
-
const executable
|
|
471
|
-
|
|
532
|
+
const { executable: resolvedExec, argsPrefix } = resolveHcloudCommand({
|
|
533
|
+
executable: options.executable || options.env?.HCLOUD_BIN,
|
|
534
|
+
executableArgs: options.executableArgs,
|
|
535
|
+
});
|
|
536
|
+
const executable = resolvedExec || 'hcloud';
|
|
537
|
+
const executableArgs = argsPrefix;
|
|
472
538
|
const cwd = options.cwd || undefined;
|
|
473
539
|
const stdin = options.stdin ?? 'y\n';
|
|
474
540
|
const childOptions = { ...options };
|
|
@@ -540,6 +606,8 @@ function runHcloudOnce(plan, options) {
|
|
|
540
606
|
ok: false,
|
|
541
607
|
code: 'TIMEOUT',
|
|
542
608
|
error: `hcloud command timed out after ${timeoutMs} ms.`,
|
|
609
|
+
exitCode: null,
|
|
610
|
+
signal: null,
|
|
543
611
|
stdout: redactOutput(stdout),
|
|
544
612
|
stderr: redactOutput(stderr),
|
|
545
613
|
plan,
|
|
@@ -558,6 +626,8 @@ function runHcloudOnce(plan, options) {
|
|
|
558
626
|
ok: false,
|
|
559
627
|
code: 'SPAWN_ERROR',
|
|
560
628
|
error: error.message,
|
|
629
|
+
exitCode: null,
|
|
630
|
+
signal: null,
|
|
561
631
|
plan,
|
|
562
632
|
});
|
|
563
633
|
});
|
|
@@ -4,6 +4,7 @@ import { homedir } from 'node:os';
|
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
|
|
6
6
|
import { compareVersion, getKooCliVersion, parseHcloudVersion } from './koocli-version.mjs';
|
|
7
|
+
import { kooCliInstallLogPath } from './preflight.mjs';
|
|
7
8
|
import { redactSecrets } from './safety-policy.mjs';
|
|
8
9
|
|
|
9
10
|
const VERSION_RE = /KooCLI|Current.*version|当前KooCLI/i;
|
|
@@ -13,7 +14,11 @@ export function findHcloudBin() {
|
|
|
13
14
|
const candidates =
|
|
14
15
|
process.platform === 'win32'
|
|
15
16
|
? [join(homedir(), 'hcloud', 'hcloud.exe')]
|
|
16
|
-
: [
|
|
17
|
+
: [
|
|
18
|
+
join(homedir(), '.local', 'bin', 'hcloud'),
|
|
19
|
+
join(homedir(), 'hcloud', 'hcloud'),
|
|
20
|
+
join('/', 'usr', 'local', 'bin', 'hcloud'),
|
|
21
|
+
];
|
|
17
22
|
const found = candidates.find((candidate) => existsSync(candidate));
|
|
18
23
|
if (found) return found;
|
|
19
24
|
|
|
@@ -148,7 +153,10 @@ export function hcloudProbeNextStep(probe) {
|
|
|
148
153
|
return 'KooCLI requires accepting its one-time privacy agreement. Run hcloud version in a real terminal and accept the prompt, then restart the agent.';
|
|
149
154
|
}
|
|
150
155
|
if (probe.status === 'not_found') {
|
|
151
|
-
return
|
|
156
|
+
return (
|
|
157
|
+
'KooCLI is not installed yet. Background auto-install may be running — please retry shortly instead of assuming failure. To install now: npx huaweicloud-devkit install-hcloud. DevKit picks up the binary from its fixed install dirs (~/.local/bin on Linux, /usr/local/bin on macOS, ~/hcloud on Windows) or HCLOUD_BIN on the next call — no agent restart needed. Install progress: ' +
|
|
158
|
+
kooCliInstallLogPath()
|
|
159
|
+
);
|
|
152
160
|
}
|
|
153
161
|
return 'Install Huawei Cloud KooCLI: npx huaweicloud-devkit install-hcloud. Configure credentials outside the agent conversation.';
|
|
154
162
|
}
|
|
@@ -9,6 +9,7 @@ import { dispatch } from './mcp-protocol.mjs';
|
|
|
9
9
|
import { DEFAULT_PORT, DEFAULT_HOST } from './mcp-server-remote.mjs';
|
|
10
10
|
import { getCachedUpdateInfo, readInstalledVersion } from './update-check.mjs';
|
|
11
11
|
import { detectAgent } from './telemetry/agent-detect.mjs';
|
|
12
|
+
import { maybePreinstallKooCli } from './preflight.mjs';
|
|
12
13
|
|
|
13
14
|
const transportIdx = process.argv.indexOf('--transport');
|
|
14
15
|
const transport = transportIdx > -1 && process.argv[transportIdx + 1] ? process.argv[transportIdx + 1] : 'stdio';
|
|
@@ -53,6 +54,14 @@ try {
|
|
|
53
54
|
if (existsSync(marker)) rmSync(marker, { force: true });
|
|
54
55
|
} catch {}
|
|
55
56
|
|
|
57
|
+
// First-use UX: if KooCLI is missing, kick off a background install so the first
|
|
58
|
+
// cloud command doesn't hit a bare "command not found". Non-blocking + log-backed.
|
|
59
|
+
// Runs at import time (before the transport branch below) so both the stdio and
|
|
60
|
+
// remote transports benefit, and `HUAWEICLOUD_SKIP_HCLOUD_PREINSTALL=1` disables it.
|
|
61
|
+
try {
|
|
62
|
+
maybePreinstallKooCli();
|
|
63
|
+
} catch {}
|
|
64
|
+
|
|
56
65
|
if (transport === 'remote') {
|
|
57
66
|
const { startRemoteServer } = await import('./mcp-server-remote.mjs');
|
|
58
67
|
startRemoteServer({ port: remotePort, host: remoteHost }).catch((error) => {
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createWriteStream, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
|
+
|
|
7
|
+
import { findHcloudBin } from './hcloud-probe.mjs';
|
|
8
|
+
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
|
|
11
|
+
// Cross-process install lock so concurrent MCP processes / sessions don't all
|
|
12
|
+
// spawn `install-hcloud` at cold start. A stale lock (older than TTL) is
|
|
13
|
+
// reclaimed; a live lock makes this process yield to the in-flight installer.
|
|
14
|
+
const INSTALL_LOCK_TTL_MS = 15 * 60 * 1000;
|
|
15
|
+
const INSTALL_LOG_MAX_BYTES = 1024 * 1024;
|
|
16
|
+
|
|
17
|
+
export function kooCliInstallLogPath() {
|
|
18
|
+
return join(homedir(), '.config', 'huaweicloud', 'logs', 'koocli-install.log');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function kooCliInstallLockPath() {
|
|
22
|
+
return join(homedir(), '.config', 'huaweicloud', '.koocli-install.lock');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Atomically acquire the cross-process install lock (exclusive create). Reclaims
|
|
26
|
+
// a stale lock. Returns { acquired: true } or { acquired: false, reason }.
|
|
27
|
+
export function acquireInstallLock(lockPath = kooCliInstallLockPath()) {
|
|
28
|
+
try {
|
|
29
|
+
mkdirSync(dirname(lockPath), { recursive: true });
|
|
30
|
+
let replace = false;
|
|
31
|
+
try {
|
|
32
|
+
const ts = Number(String(readFileSync(lockPath, 'utf8')).trim());
|
|
33
|
+
if (Number.isFinite(ts) && Date.now() - ts > INSTALL_LOCK_TTL_MS) replace = true;
|
|
34
|
+
} catch {}
|
|
35
|
+
if (replace) {
|
|
36
|
+
try {
|
|
37
|
+
rmSync(lockPath, { force: true });
|
|
38
|
+
} catch (error) {
|
|
39
|
+
return { acquired: false, reason: `stale lock cleanup failed: ${error.message}` };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
writeFileSync(lockPath, String(Date.now()), { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
43
|
+
return { acquired: true };
|
|
44
|
+
} catch (error) {
|
|
45
|
+
return {
|
|
46
|
+
acquired: false,
|
|
47
|
+
reason: error.code === 'EEXIST' ? 'another process is installing' : `lock error: ${error.message}`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function releaseInstallLock(lockPath = kooCliInstallLockPath()) {
|
|
53
|
+
try {
|
|
54
|
+
rmSync(lockPath, { force: true });
|
|
55
|
+
} catch {}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Open the install log, rotating (reset) once it exceeds INSTALL_LOG_MAX_BYTES
|
|
59
|
+
// and pinning 0600 so the path holds no sensitive mount-down rights for others.
|
|
60
|
+
export function openKooCliInstallLog(logPath = kooCliInstallLogPath()) {
|
|
61
|
+
try {
|
|
62
|
+
const st = existsSync(logPath) ? statSync(logPath) : null;
|
|
63
|
+
if (st && st.size > INSTALL_LOG_MAX_BYTES) rmSync(logPath, { force: true });
|
|
64
|
+
mkdirSync(dirname(logPath), { recursive: true });
|
|
65
|
+
} catch {}
|
|
66
|
+
return createWriteStream(logPath, { flags: 'a', mode: 0o600 });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Entry that dispatches `install-hcloud` into this file's own `main()` on import,
|
|
70
|
+
// exactly like bin/setup.cjs — but resolved as a sibling (src/setup-cli.mjs) so it
|
|
71
|
+
// works in copied install trees where the repo-top bin/ directory is absent.
|
|
72
|
+
export function resolvePreinstallCliEntry() {
|
|
73
|
+
return join(__dirname, 'setup-cli.mjs');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let installStarted = false;
|
|
77
|
+
|
|
78
|
+
// Starts a one-shot background install of KooCLI when it is missing. Never throws
|
|
79
|
+
// and never blocks: on failure the child's output is captured in the log file and
|
|
80
|
+
// the session continues normally (the friendly not_found hint will point at the log).
|
|
81
|
+
//
|
|
82
|
+
// Trust boundary (P4): this runs `install-hcloud` unattended, i.e. it downloads and
|
|
83
|
+
// executes the official HTTPS installer without checksum verification. Only enable
|
|
84
|
+
// on trusted networks / over HTTPS; do not bypass with proxies you don't control.
|
|
85
|
+
export function maybePreinstallKooCli({ force = false } = {}) {
|
|
86
|
+
if (installStarted && !force) return { launched: false, reason: 'already_launched' };
|
|
87
|
+
if (process.env.HUAWEICLOUD_SKIP_HCLOUD_PREINSTALL === '1') {
|
|
88
|
+
return { launched: false, reason: 'disabled_by_env' };
|
|
89
|
+
}
|
|
90
|
+
if (findHcloudBin()) {
|
|
91
|
+
return { launched: false, reason: 'already installed' };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const lock = acquireInstallLock();
|
|
95
|
+
if (!lock.acquired) {
|
|
96
|
+
return { launched: false, reason: lock.reason };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
installStarted = true;
|
|
100
|
+
const entryPath = resolvePreinstallCliEntry();
|
|
101
|
+
if (!existsSync(entryPath)) {
|
|
102
|
+
releaseInstallLock();
|
|
103
|
+
console.error(`[preflight] install entry missing: ${entryPath}`);
|
|
104
|
+
return { launched: false, reason: `install entry missing: ${entryPath}` };
|
|
105
|
+
}
|
|
106
|
+
const entry = pathToFileURL(entryPath).href;
|
|
107
|
+
const log = openKooCliInstallLog();
|
|
108
|
+
log.on('error', () => {});
|
|
109
|
+
const release = () => {
|
|
110
|
+
log.end();
|
|
111
|
+
releaseInstallLock();
|
|
112
|
+
};
|
|
113
|
+
const child = spawn(process.execPath, [entry, 'install-hcloud'], {
|
|
114
|
+
stdio: ['ignore', log, log],
|
|
115
|
+
detached: true,
|
|
116
|
+
windowsHide: true,
|
|
117
|
+
});
|
|
118
|
+
child.unref();
|
|
119
|
+
child.on('error', (err) => {
|
|
120
|
+
log.write(`[preflight] spawn failed: ${err.message}\n`);
|
|
121
|
+
release();
|
|
122
|
+
});
|
|
123
|
+
child.on('close', () => release());
|
|
124
|
+
return { launched: true, reason: 'background install started' };
|
|
125
|
+
}
|
|
@@ -39,7 +39,96 @@ export function clearProxyConfig() {
|
|
|
39
39
|
return true;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
function
|
|
42
|
+
function ipv4ToInt(ip) {
|
|
43
|
+
const parts = ip.split('.');
|
|
44
|
+
if (parts.length !== 4) return null;
|
|
45
|
+
let n = 0;
|
|
46
|
+
for (const part of parts) {
|
|
47
|
+
if (!/^\d{1,3}$/.test(part)) return null;
|
|
48
|
+
const v = Number(part);
|
|
49
|
+
if (v > 255) return null;
|
|
50
|
+
n = n * 256 + v;
|
|
51
|
+
}
|
|
52
|
+
return BigInt(n);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function ipv6ToBigInt(ip) {
|
|
56
|
+
let addr = ip.toLowerCase();
|
|
57
|
+
if (addr === '::') addr = '0:0:0:0:0:0:0:0';
|
|
58
|
+
const [head, tail] = addr.split('::');
|
|
59
|
+
const headGroups = head ? head.split(':').filter(Boolean) : [];
|
|
60
|
+
const tailGroups = tail ? tail.split(':').filter(Boolean) : [];
|
|
61
|
+
if (addr.includes('::')) {
|
|
62
|
+
const missing = 8 - headGroups.length - tailGroups.length;
|
|
63
|
+
if (missing < 0) return null;
|
|
64
|
+
const groups = [...headGroups, ...Array(missing).fill('0'), ...tailGroups];
|
|
65
|
+
if (groups.length !== 8) return null;
|
|
66
|
+
return groupsToBigInt(groups);
|
|
67
|
+
}
|
|
68
|
+
const groups = head ? head.split(':') : [];
|
|
69
|
+
if (groups.length !== 8) return null;
|
|
70
|
+
return groupsToBigInt(groups);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function groupsToBigInt(groups) {
|
|
74
|
+
let n = 0n;
|
|
75
|
+
for (const g of groups) {
|
|
76
|
+
if (!/^[0-9a-f]{1,4}$/.test(g)) return null;
|
|
77
|
+
n = (n << 16n) | BigInt(parseInt(g, 16));
|
|
78
|
+
}
|
|
79
|
+
return n;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function ipv4MappedV6ToBigInt(ip) {
|
|
83
|
+
// ::ffff:a.b.c.d — IPv4-mapped IPv6. Compare against IPv4 CIDR too.
|
|
84
|
+
const m = ip.match(/^::ffff:([0-9.]+)$/i);
|
|
85
|
+
if (!m) return null;
|
|
86
|
+
const v4 = ipv4ToInt(m[1]);
|
|
87
|
+
if (v4 === null) return null;
|
|
88
|
+
return v4;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function parseCidr(pattern) {
|
|
92
|
+
const slash = pattern.lastIndexOf('/');
|
|
93
|
+
if (slash < 0) return null;
|
|
94
|
+
const ipPart = pattern.slice(0, slash);
|
|
95
|
+
const maskPart = pattern.slice(slash + 1);
|
|
96
|
+
if (!/^\d{1,3}$/.test(maskPart)) return null;
|
|
97
|
+
const prefix = Number(maskPart);
|
|
98
|
+
if (ipPart.includes(':')) {
|
|
99
|
+
const mapped = ipv4MappedV6ToBigInt(ipPart);
|
|
100
|
+
if (mapped !== null) {
|
|
101
|
+
if (prefix < 0 || prefix > 32) return null;
|
|
102
|
+
const mask = prefix === 0 ? 0n : ((1n << 32n) - 1n) ^ ((1n << (32n - BigInt(prefix))) - 1n);
|
|
103
|
+
return { bits: 32n, base: mapped, mask };
|
|
104
|
+
}
|
|
105
|
+
const base = ipv6ToBigInt(ipPart);
|
|
106
|
+
if (base === null) return null;
|
|
107
|
+
if (prefix < 0 || prefix > 128) return null;
|
|
108
|
+
const mask = prefix === 0 ? 0n : ((1n << 128n) - 1n) ^ ((1n << (128n - BigInt(prefix))) - 1n);
|
|
109
|
+
return { bits: 128n, base, mask };
|
|
110
|
+
}
|
|
111
|
+
const base = ipv4ToInt(ipPart);
|
|
112
|
+
if (base === null) return null;
|
|
113
|
+
if (prefix < 0 || prefix > 32) return null;
|
|
114
|
+
const mask = prefix === 0 ? 0n : ((1n << 32n) - 1n) ^ ((1n << (32n - BigInt(prefix))) - 1n);
|
|
115
|
+
return { bits: 32n, base, mask };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function ipInCidr(hostname, cidr) {
|
|
119
|
+
if (cidr.bits === 32n) {
|
|
120
|
+
const v4 = ipv4ToInt(hostname);
|
|
121
|
+
if (v4 !== null) return (v4 & cidr.mask) === (cidr.base & cidr.mask);
|
|
122
|
+
const mapped = ipv4MappedV6ToBigInt(hostname);
|
|
123
|
+
if (mapped !== null) return (mapped & cidr.mask) === (cidr.base & cidr.mask);
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
const v6 = ipv6ToBigInt(hostname);
|
|
127
|
+
if (v6 !== null) return (v6 & cidr.mask) === (cidr.base & cidr.mask);
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function shouldBypassProxy(hostname, noProxyList) {
|
|
43
132
|
if (!noProxyList.length) return false;
|
|
44
133
|
const lower = hostname.toLowerCase();
|
|
45
134
|
for (const pattern of noProxyList) {
|
|
@@ -47,6 +136,10 @@ function shouldBypassProxy(hostname, noProxyList) {
|
|
|
47
136
|
if (!p) continue;
|
|
48
137
|
if (p === lower) return true;
|
|
49
138
|
if (p === '*') return true;
|
|
139
|
+
if (p.includes('/')) {
|
|
140
|
+
const cidr = parseCidr(p);
|
|
141
|
+
if (cidr && ipInCidr(lower, cidr)) return true;
|
|
142
|
+
}
|
|
50
143
|
if (p.startsWith('*.')) {
|
|
51
144
|
const domain = p.slice(1);
|
|
52
145
|
if (lower.endsWith(domain) || lower === p.slice(2)) return true;
|
|
@@ -19,7 +19,7 @@ export function loadRiskRules(options = {}) {
|
|
|
19
19
|
function redactEvidence(text) {
|
|
20
20
|
return String(text)
|
|
21
21
|
.replace(
|
|
22
|
-
/((?:access[_-]?key|secret[_-]?key|security[_-]?token|x[_-]?auth[_-]?token|token|authorization|password|passwd|admin[_-]?pass|credential)\s*[:=]\s*)("[^"]*"|'[^']*'|[^\s,;]+)/gi,
|
|
22
|
+
/((?:access[_-]?key|secret[_-]?key|secret|security[_-]?token|x[_-]?auth[_-]?token|token|authorization|password|passwd|admin[_-]?pass|credential)\s*[:=]\s*)("[^"]*"|'[^']*'|[^\s,;]+)/gi,
|
|
23
23
|
'$1<redacted>',
|
|
24
24
|
)
|
|
25
25
|
.replace(/(AK|SK)\s*[:=]\s*("[^"]*"|'[^']*'|[^\s,;]+)/g, '$1=<redacted>');
|