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.
Files changed (32) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/package.json +2 -1
  4. package/plugins/huaweicloud-core/.claude-plugin/plugin.json +1 -1
  5. package/plugins/huaweicloud-core/.codex-plugin/plugin.json +1 -1
  6. package/plugins/huaweicloud-core/.cursor-plugin/plugin.json +1 -1
  7. package/plugins/huaweicloud-core/.hermes-plugin/plugin.json +1 -1
  8. package/plugins/huaweicloud-core/.workbuddy-plugin/plugin.json +1 -1
  9. package/plugins/huaweicloud-core/hooks/huaweicloud-safety.py +19 -1
  10. package/plugins/huaweicloud-core/openclaw.plugin.json +1 -1
  11. package/plugins/huaweicloud-core/safety/policy.json +1 -1
  12. package/plugins/huaweicloud-core/safety/rules/cloud-risk-rules.json +57 -2
  13. package/plugins/huaweicloud-core/skills/huawei-billing/SKILL.md +19 -16
  14. package/plugins/huaweicloud-core/skills/huawei-modelarts/SKILL.md +1 -1
  15. package/plugins/huaweicloud-core/skills/huawei-sandbox/SKILL.md +9 -3
  16. package/plugins/huaweicloud-core/skills/huaweicloud-capability-discovery/SKILL.md +1 -1
  17. package/plugins/huaweicloud-core/skills/huaweicloud-cli-and-auth/SKILL.md +15 -2
  18. package/plugins/huaweicloud-core/src/auth/service.mjs +36 -0
  19. package/plugins/huaweicloud-core/src/data/icons-manifest.v1.json +23 -5
  20. package/plugins/huaweicloud-core/src/detect-framework.mjs +41 -5
  21. package/plugins/huaweicloud-core/src/hcloud-cli.mjs +78 -8
  22. package/plugins/huaweicloud-core/src/hcloud-probe.mjs +10 -2
  23. package/plugins/huaweicloud-core/src/mcp-server.mjs +9 -0
  24. package/plugins/huaweicloud-core/src/preflight.mjs +125 -0
  25. package/plugins/huaweicloud-core/src/proxy/proxy-config.mjs +94 -1
  26. package/plugins/huaweicloud-core/src/risk-rule-engine.mjs +1 -1
  27. package/plugins/huaweicloud-core/src/safety-policy.mjs +165 -33
  28. package/plugins/huaweicloud-core/src/sandbox/session-manager.mjs +214 -27
  29. package/plugins/huaweicloud-core/src/search-market.mjs +40 -1
  30. package/plugins/huaweicloud-core/src/setup-cli.mjs +57 -17
  31. package/plugins/huaweicloud-core/src/tools.mjs +307 -20
  32. 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 { findHcloudBin, resolveHcloudCommand } from './hcloud-probe.mjs';
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
- delete map[token];
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 = options.executable || options.env?.HCLOUD_BIN || discoverHcloudPath() || 'hcloud';
471
- const executableArgs = Array.isArray(options.executableArgs) ? options.executableArgs.map(String) : [];
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
- : [join(homedir(), '.local', 'bin', 'hcloud'), join(homedir(), 'hcloud', 'hcloud')];
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 'hcloud executable not found. Set HCLOUD_BIN to the full hcloud path, or install KooCLI: npx huaweicloud-devkit install-hcloud. Then restart the agent.';
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 shouldBypassProxy(hostname, noProxyList) {
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>');