huaweicloud-devkit 1.1.4-next.3 → 1.1.4-next.5

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "huaweicloud-devkit",
3
3
  "mcpName": "io.github.huaweicloud/huaweicloud-devkit",
4
- "version": "1.1.4-next.3",
4
+ "version": "1.1.4-next.5",
5
5
  "kooCliVersion": "7.2.12",
6
6
  "description": "Agent toolkit that helps coding agents use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities safely and accurately.",
7
7
  "type": "module",
@@ -17,7 +17,7 @@
17
17
  "mcpServers": "./.mcp.json",
18
18
  "description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
19
19
  "skills": "./skills/",
20
- "version": "1.1.4-next.3",
20
+ "version": "1.1.4-next.5",
21
21
  "author": {
22
22
  "name": "HuaweiCloud Mate",
23
23
  "url": "https://github.com/huaweicloud"
@@ -20,7 +20,7 @@
20
20
  "mcpServers": "./.mcp.json",
21
21
  "description": "Agent toolkit that helps coding agents use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities safely and accurately.",
22
22
  "skills": "./skills/",
23
- "version": "1.1.4-next.3",
23
+ "version": "1.1.4-next.5",
24
24
  "author": {
25
25
  "name": "HuaweiCloud Mate",
26
26
  "url": "https://github.com/huaweicloud"
@@ -17,7 +17,7 @@
17
17
  "mcpServers": "./.mcp.json",
18
18
  "description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
19
19
  "skills": "./skills/",
20
- "version": "1.1.4-next.3",
20
+ "version": "1.1.4-next.5",
21
21
  "author": {
22
22
  "name": "HuaweiCloud Mate",
23
23
  "url": "https://github.com/huaweicloud"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "huaweicloud-devkit",
3
- "version": "1.1.4-next.3",
3
+ "version": "1.1.4-next.5",
4
4
  "description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
5
5
  "author": {
6
6
  "name": "HuaweiCloud Mate",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "huaweicloud-devkit",
3
- "version": "1.1.4-next.3",
3
+ "version": "1.1.4-next.5",
4
4
  "description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
5
5
  "author": {
6
6
  "name": "HuaweiCloud Mate",
@@ -2,7 +2,7 @@
2
2
  "name": "huaweicloud-devkit",
3
3
  "id": "huaweicloud-devkit",
4
4
  "displayName": "HuaweiCloud DevKit",
5
- "version": "1.1.4-next.3",
5
+ "version": "1.1.4-next.5",
6
6
  "family": "bundle-plugin",
7
7
  "bundleFormat": "codex",
8
8
  "description": "Guide coding agents to use Huawei Cloud safely — KooCLI, APIs, SDKs, 28 MCP tools, skills, and safety guardrails.",
@@ -41,7 +41,7 @@ Domain expertise for Huawei Cloud Object Storage Service (OBS). Covers bucket/ob
41
41
  | Three-layer permissions | IAM > Bucket Policy > ACL. Most restrictive wins |
42
42
  | Versioning is irreversible | Once enabled, cannot be disabled, only suspended |
43
43
  | OBS uses AK/SK directly | NOT IAM tokens. Auth errors mean check AK/SK validity |
44
- | Static website via CLI missing | KooCLI OBS lacks website config. Use REST API or console |
44
+ | Static website via CLI missing | KooCLI OBS lacks website config. Use `huaweicloud_obs_set_website_config` MCP tool (handles AWS4 signing internally) or console |
45
45
  | **OBS needs separate cred config** | `hcloud configure` is NOT enough for OBS. Before any OBS operation, call `huaweicloud_setup_obs_config` to sync credentials from hcloud profile. |
46
46
  | **obsutil interactive prompts** | `cp`/`rm` without `-f` causes "Please input (y/n)" → Agent hangs (TIMEOUT). Always use `-f` for non-interactive. |
47
47
  | **Directory upload adds prefix** | `cp <dir>/ obs://<bucket>/ -r` puts files under `bucket/<dir>/...`. Use `-flat` for root-level files (static sites). Preview with `-dryRun` first. |
@@ -83,7 +83,7 @@ hcloud OBS config -e=<endpoint> -i=<AK> -k=<SK> -t=token
83
83
  See `references/static-website.md` for the full end-to-end workflow:
84
84
  Build → Create bucket → Upload → Set bucket ACL → Set object ACL → Configure website (REST API/console)
85
85
 
86
- > KooCLI OBS does NOT support `SetBucketWebsite`. Configure static website hosting via REST API (`PUT /?website`) or the Huawei Cloud console.
86
+ > KooCLI OBS does NOT support `SetBucketWebsite`. Use the `huaweicloud_obs_set_website_config` MCP tool (handles AWS4 signing internally, no manual signature needed) or the Huawei Cloud console.
87
87
 
88
88
  ## Single-File Quick Share
89
89
 
@@ -31,13 +31,9 @@ hcloud OBS chattri obs://my-static-site -acl=public-read
31
31
  hcloud OBS chattri obs://my-static-site/ -r -f -acl=public-read
32
32
 
33
33
  # 5. Configure static website hosting
34
- # KooCLI OBS does NOT support this. Use one of:
35
- # Option A — REST API:
36
- # PUT /?website HTTP/1.1
37
- # Host: my-static-site.obs.cn-north-4.myhuaweicloud.com
38
- # Body: {"IndexDocument": {"Suffix": "index.html"}, "ErrorDocument": {"Key": "error.html"}}
39
- # (requires AK/SK signature)
40
- # Option B — Huawei Cloud Console:
34
+ # KooCLI OBS does NOT support this. Use the MCP tool (handles AWS4 signing internally):
35
+ # action=set, bucket=<bucket>, region=<region>, indexDocument=index.html, errorDocument=404.html
36
+ # Or use Huawei Cloud Console:
41
37
  # Console → OBS → Bucket → Basic Settings → Static Website Hosting
42
38
 
43
39
  # 6. Verify
@@ -58,6 +58,33 @@ export function readGlobalCredentials() {
58
58
  }
59
59
  }
60
60
 
61
+ // R11: Placeholder / masked credential values are treated as "not configured".
62
+ // A vendor/IDE marketplace that pre-fills mcp_settings env with template text
63
+ // (e.g. "<HW_ACCESS_KEY>" or "${SECRET_KEY}") must not shadow the user's real
64
+ // S1 vault. Real AK/SK are >=20 char alphanumeric strings, so none of the
65
+ // template/masked patterns below can match a genuine credential.
66
+ export function isPlaceholder(value) {
67
+ if (typeof value !== 'string' || value.length === 0) return false;
68
+ // <HW_ACCESS_KEY> / <your-ak> / <...>
69
+ if (/^<[^>]*>$/.test(value)) return true;
70
+ // ${HW_ACCESS_KEY}
71
+ if (/^\$\{[^}]*\}$/.test(value)) return true;
72
+ // YOUR_AK / ACCESS_KEY / SECRET_KEY / SECURITY_TOKEN / REGION (bare keyword)
73
+ if (/^(YOUR_)?(AK|SK|ACCESS[_-]?KEY|SECRET[_-]?KEY|TOKEN|SECURITY[_-]?TOKEN|REGION)$/i.test(value)) return true;
74
+ // placeholder / replace_me / change.me
75
+ if (/\b(placeholder|replace[._-]?me|change[._-]?me)\b/i.test(value)) return true;
76
+ // masked value: short alnum prefix + asterisk run (abc**** / token****) — the
77
+ // S4 mirror writes the SK slot this way so the secret never lands in plaintext.
78
+ if (/^[A-Za-z0-9]{3,8}\*+$/.test(value)) return true;
79
+ // bare asterisks
80
+ if (/^\*+$/.test(value)) return true;
81
+ return false;
82
+ }
83
+
84
+ function present(v) {
85
+ return typeof v === 'string' && v.length > 0 && !isPlaceholder(v);
86
+ }
87
+
61
88
  export function writeGlobalCredentials(credentials = {}) {
62
89
  const path = globalCredentialsPath();
63
90
  mkdirSync(dirname(path), { recursive: true });
@@ -99,9 +126,10 @@ export function writeObsConfig(credentials = {}) {
99
126
  }
100
127
 
101
128
  export function resolveCredentials(options = {}) {
102
- let ak = process.env.HW_ACCESS_KEY;
103
- let sk = process.env.HW_SECRET_KEY;
104
- let securityToken = process.env.HW_SECURITY_TOKEN;
129
+ // R11: placeholder/masked env values are "not configured", never credentials.
130
+ let ak = present(process.env.HW_ACCESS_KEY) ? process.env.HW_ACCESS_KEY : '';
131
+ let sk = present(process.env.HW_SECRET_KEY) ? process.env.HW_SECRET_KEY : '';
132
+ let securityToken = present(process.env.HW_SECURITY_TOKEN) ? process.env.HW_SECURITY_TOKEN : '';
105
133
  let region = process.env.HW_REGION || process.env.HUAWEICLOUD_REGION || '';
106
134
 
107
135
  const codeartsCreds = isCodeArtsContext() ? readCodeArtsCredentials() : null;
@@ -113,36 +141,33 @@ export function resolveCredentials(options = {}) {
113
141
 
114
142
  const stored = readGlobalCredentials();
115
143
  if (stored) {
116
- if (!ak && stored.ak) ak = stored.ak;
117
- if (!sk && stored.sk) sk = stored.sk;
118
- if (!securityToken && stored.securityToken) securityToken = stored.securityToken;
144
+ // R11: skip placeholder/masked values from the vault too (templates must
145
+ // not win the truthiness check and shadow another store).
146
+ if (!ak && present(stored.ak)) ak = stored.ak;
147
+ if (!sk && present(stored.sk)) sk = stored.sk;
148
+ if (!securityToken && present(stored.securityToken)) securityToken = stored.securityToken;
119
149
  if (!region && stored.region) region = stored.region;
120
150
  }
121
151
 
122
152
  // R9: S1 written by `auth_switch persist` (configuredBySession) is the session's
123
153
  // source of truth and wins over env-injected defaults. Plain S1 (`auth init`)
124
154
  // still yields to env so devspace-style env injection keeps working.
125
- if (stored && stored.configuredBySession === true && stored.ak && stored.sk) {
155
+ if (stored && stored.configuredBySession === true && present(stored.ak) && present(stored.sk)) {
126
156
  ak = stored.ak;
127
157
  sk = stored.sk;
128
- if (!securityToken) securityToken = stored.securityToken || '';
158
+ if (!securityToken) securityToken = present(stored.securityToken) ? stored.securityToken : '';
129
159
  if (!region) region = stored.region || '';
130
160
  }
131
161
 
132
162
  // Sandbox/platform-injected temporary STS credentials (env vars carrying a
133
163
  // security token) must not shadow the user's explicit permanent credentials
134
164
  // from `auth init`. Prefer the stored file when both exist.
135
- if (
136
- process.env.HW_ACCESS_KEY &&
137
- process.env.HW_SECRET_KEY &&
138
- process.env.HW_SECURITY_TOKEN &&
139
- stored &&
140
- stored.ak &&
141
- stored.sk
142
- ) {
165
+ const envHasFullTriplet =
166
+ present(process.env.HW_ACCESS_KEY) && present(process.env.HW_SECRET_KEY) && present(process.env.HW_SECURITY_TOKEN);
167
+ if (envHasFullTriplet && stored && present(stored.ak) && present(stored.sk)) {
143
168
  ak = stored.ak;
144
169
  sk = stored.sk;
145
- securityToken = stored.securityToken || '';
170
+ securityToken = present(stored.securityToken) ? stored.securityToken : '';
146
171
  region = stored.region || region;
147
172
  }
148
173
 
@@ -189,16 +214,16 @@ export function readCodeArtsCredentials() {
189
214
  try {
190
215
  if (!existsSync(path)) continue;
191
216
  const config = JSON.parse(readFileSync(path, 'utf8'));
192
- const server = config?.mcpServers?.['huaweicloud-devkit'];
217
+ const server = config?.mcpServers?.['huaweicloud-devkit'] || config?.mcpServers?.['HuaweiCloud DevKit'];
193
218
  if (!server?.env) continue;
194
219
 
195
220
  const ak = server.env.HW_ACCESS_KEY;
196
221
  const sk = server.env.HW_SECRET_KEY;
197
- if (ak && sk) {
222
+ if (present(ak) && present(sk)) {
198
223
  return {
199
224
  ak,
200
225
  sk,
201
- securityToken: server.env.HW_SECURITY_TOKEN || '',
226
+ securityToken: present(server.env.HW_SECURITY_TOKEN) ? server.env.HW_SECURITY_TOKEN : '',
202
227
  region: server.env.HW_REGION || server.env.HUAWEICLOUD_REGION || '',
203
228
  };
204
229
  }
@@ -213,15 +238,15 @@ export function readCodeArtsCredentials() {
213
238
  try {
214
239
  if (existsSync(path)) {
215
240
  const config = JSON.parse(readFileSync(path, 'utf8'));
216
- const server = config?.mcp?.['huaweicloud-devkit'];
241
+ const server = config?.mcp?.['huaweicloud-devkit'] || config?.mcp?.['HuaweiCloud DevKit'];
217
242
  if (server?.environment) {
218
243
  const ak = server.environment.HW_ACCESS_KEY;
219
244
  const sk = server.environment.HW_SECRET_KEY;
220
- if (ak && sk) {
245
+ if (present(ak) && present(sk)) {
221
246
  return {
222
247
  ak,
223
248
  sk,
224
- securityToken: server.environment.HW_SECURITY_TOKEN || '',
249
+ securityToken: present(server.environment.HW_SECURITY_TOKEN) ? server.environment.HW_SECURITY_TOKEN : '',
225
250
  region: server.environment.HW_REGION || server.environment.HUAWEICLOUD_REGION || '',
226
251
  };
227
252
  }
@@ -6,6 +6,7 @@ import { spawnSync } from 'node:child_process';
6
6
 
7
7
  import {
8
8
  hasRuntimeCredentials,
9
+ isPlaceholder,
9
10
  obsConfigPath,
10
11
  readGlobalCredentials,
11
12
  readLastSync,
@@ -104,10 +105,13 @@ function s2CurrentMatchesLastDevkitSync(kooCli, s1, s1Fingerprint) {
104
105
 
105
106
  export function scanState() {
106
107
  const s1 = readGlobalCredentials() || {};
107
- const envAk = process.env.HW_ACCESS_KEY || '';
108
- const envSk = process.env.HW_SECRET_KEY || '';
108
+ const envAk = isPlaceholder(process.env.HW_ACCESS_KEY) ? '' : process.env.HW_ACCESS_KEY || '';
109
+ const envSk = isPlaceholder(process.env.HW_SECRET_KEY) ? '' : process.env.HW_SECRET_KEY || '';
109
110
  const kooCli = readKooCliProfiles();
110
111
  const inconsistencies = [];
112
+ const envIsPlaceholder =
113
+ Boolean(process.env.HW_ACCESS_KEY && isPlaceholder(process.env.HW_ACCESS_KEY)) ||
114
+ Boolean(process.env.HW_SECRET_KEY && isPlaceholder(process.env.HW_SECRET_KEY));
111
115
 
112
116
  const s1Fingerprint = fingerprint(s1.ak, s1.sk);
113
117
  const currentFp = currentFingerprintFromHcloud(kooCli);
@@ -153,6 +157,9 @@ export function scanState() {
153
157
  runtimeFingerprint,
154
158
  },
155
159
  kooCliCurrent: kooCli.error ? null : kooCli.current,
160
+ // R11: env carries placeholder/masked values → surface that explicitly so
161
+ // diagnostics don't show a bogus fingerprint derived from template text.
162
+ envIsPlaceholder,
156
163
  // S2 is encrypted storage (authEncrypt) → its fingerprint is unavailable,
157
164
  // so the S2-current drift check is intentionally skipped (#533).
158
165
  s2Encrypted: Boolean(!kooCli.error && kooCli.authEncrypt),
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url';
4
4
  import { homedir, tmpdir } from 'node:os';
5
5
  import { spawnSync, execFile } from 'node:child_process';
6
6
  import { promisify } from 'node:util';
7
+ import { createHmac, createHash } from 'node:crypto';
7
8
 
8
9
  import { evaluateArtifacts, evaluateCommandRisk, evaluateDeployPlan } from './risk-rule-engine.mjs';
9
10
  import { classifyTextCommand, redactSecrets } from './safety-policy.mjs';
@@ -48,6 +49,7 @@ import {
48
49
  resolveCredentialsWithRuntime,
49
50
  } from './auth/credentials.mjs';
50
51
  import { trackToolInvoke, trackSkillRetrieve, clearUserHash } from './telemetry/telemetry.mjs';
52
+ import { fetchWithProxy } from './proxy/proxy-agent.mjs';
51
53
  import { fingerprint, runHcloudConfigure, resolveManagedProfile } from './auth/reconcile.mjs';
52
54
  import {
53
55
  getCachedUpdateInfo,
@@ -846,6 +848,32 @@ export const TOOL_DEFINITIONS = [
846
848
  },
847
849
  },
848
850
  },
851
+ {
852
+ name: 'huaweicloud_obs_set_website_config',
853
+ description:
854
+ '配置 OBS 桶的静态网站托管。KooCLI OBS 不支持 SetBucketWebsite API,此工具内部实现 AWS4 签名调用 OBS REST API,屏蔽签名细节。支持 set(配置)、get(查询)、delete(删除)三种操作。操作前需确保桶已创建且已设置 public-read ACL。',
855
+ inputSchema: {
856
+ type: 'object',
857
+ properties: {
858
+ action: {
859
+ type: 'string',
860
+ enum: ['set', 'get', 'delete'],
861
+ description: '操作类型:set=配置静态网站托管,get=查询当前配置,delete=删除配置',
862
+ },
863
+ bucket: { type: 'string', description: 'OBS 桶名称' },
864
+ region: { type: 'string', description: 'OBS 桶所在区域,如 cn-north-4' },
865
+ indexDocument: {
866
+ type: 'string',
867
+ description: '首页文件名(action=set 时必填),如 index.html',
868
+ },
869
+ errorDocument: {
870
+ type: 'string',
871
+ description: '错误页面文件名(action=set 时可选),如 404.html 或 error.html',
872
+ },
873
+ },
874
+ required: ['action', 'bucket', 'region'],
875
+ },
876
+ },
849
877
  ];
850
878
 
851
879
  function toolInvokeValue(name, args) {
@@ -1448,19 +1476,26 @@ export async function callTool(name, rawArgs = {}, opts = {}) {
1448
1476
  case 'huaweicloud_voucher_claim':
1449
1477
  return await hdkitVoucherClaim(args.domain_id);
1450
1478
  case 'huaweicloud_check_update':
1451
- return await handleCheckUpdate(args, { sessionId: opts?.sessionId });
1479
+ return await handleCheckUpdate(args, { sessionId: opts?.sessionId, doQuery: opts?.doQuery });
1452
1480
  case 'huaweicloud_upgrade':
1453
1481
  return await handleUpgrade(args, { sessionId: opts?.sessionId });
1482
+ case 'huaweicloud_obs_set_website_config':
1483
+ return await handleObsWebsiteConfig(args);
1454
1484
  default:
1455
1485
  throw new Error(`Unknown tool: ${name}`);
1456
1486
  }
1457
1487
  }
1458
1488
 
1459
1489
  async function handleCheckUpdate(args = {}, opts = {}) {
1460
- const sessionId = opts?.sessionId || null;
1490
+ const { sessionId = null, doQuery } = opts;
1461
1491
  const current = readInstalledVersion() || '0.0.0';
1462
1492
  if (args.dismiss === true) {
1463
- const distTags = await getUpdateDistTags(current, { sessionId });
1493
+ const distTags = await getUpdateDistTags(current, { sessionId, doQuery });
1494
+ if (!distTags) {
1495
+ // 查询失败:不降级为 current 冷却、不写 skip,返回 check_failed。
1496
+ // 不调 invalidateUpdateCache(),以保留 failedAt 的 5 分钟失败节流。
1497
+ return judgeUpdate(current, null, null);
1498
+ }
1464
1499
  const target = determineTarget(current, distTags);
1465
1500
  const dismissedVersion =
1466
1501
  typeof args.dismissVersion === 'string' && args.dismissVersion ? args.dismissVersion : target || current;
@@ -1468,7 +1503,7 @@ async function handleCheckUpdate(args = {}, opts = {}) {
1468
1503
  invalidateUpdateCache();
1469
1504
  return judgeUpdate(current, distTags, state);
1470
1505
  }
1471
- return getCachedUpdateInfo(current, { sessionId });
1506
+ return getCachedUpdateInfo(current, { sessionId, doQuery });
1472
1507
  }
1473
1508
 
1474
1509
  async function handleUpgrade(args = {}, opts = {}) {
@@ -2255,3 +2290,112 @@ async function getRegionalAvailability(service, region) {
2255
2290
  export function classifyRawCommand(command) {
2256
2291
  return classifyTextCommand(command);
2257
2292
  }
2293
+
2294
+ // ── OBS Static Website Hosting (AWS4 signed REST API) ──
2295
+
2296
+ async function handleObsWebsiteConfig(args) {
2297
+ const { action, bucket, region, indexDocument, errorDocument } = args;
2298
+ if (!bucket || !region) {
2299
+ throw new Error('bucket and region are required');
2300
+ }
2301
+ const creds = resolveCredentialsWithRuntime({});
2302
+ if (!creds?.ak || !creds?.sk) {
2303
+ throw new Error('OBS website config requires AK/SK credentials. Run huaweicloud_auth_init first.');
2304
+ }
2305
+
2306
+ const host = `${bucket}.obs.${region}.myhuaweicloud.com`;
2307
+ const endpoint = `https://${host}`;
2308
+
2309
+ if (action === 'get') {
2310
+ const res = await obsSignedRequest('GET', endpoint, '/?website', '', creds, region);
2311
+ return { ok: res.status === 200, status: res.status, body: res.body };
2312
+ }
2313
+
2314
+ if (action === 'delete') {
2315
+ const res = await obsSignedRequest('DELETE', endpoint, '/?website', '', creds, region);
2316
+ return { ok: res.status === 204, status: res.status };
2317
+ }
2318
+
2319
+ if (action === 'set') {
2320
+ if (!indexDocument) {
2321
+ throw new Error('indexDocument is required for action=set');
2322
+ }
2323
+ const xmlParts = ['<WebsiteConfiguration>', ` <IndexDocument><Suffix>${indexDocument}</Suffix></IndexDocument>`];
2324
+ if (errorDocument) {
2325
+ xmlParts.push(` <ErrorDocument><Key>${errorDocument}</Key></ErrorDocument>`);
2326
+ }
2327
+ xmlParts.push('</WebsiteConfiguration>');
2328
+ const body = xmlParts.join('\n');
2329
+ const res = await obsSignedRequest('PUT', endpoint, '/?website', body, creds, region);
2330
+ const websiteUrl = `http://${bucket}.obs-website.${region}.myhuaweicloud.com`;
2331
+ return {
2332
+ ok: res.status === 200,
2333
+ status: res.status,
2334
+ websiteUrl,
2335
+ message:
2336
+ res.status === 200
2337
+ ? `Static website hosting configured. Website URL: ${websiteUrl} (may take ~1 min to propagate)`
2338
+ : `Failed to configure website: HTTP ${res.status}`,
2339
+ };
2340
+ }
2341
+
2342
+ throw new Error(`Unknown action: ${action}. Use set, get, or delete.`);
2343
+ }
2344
+
2345
+ async function obsSignedRequest(method, endpoint, pathAndQuery, body, creds, region) {
2346
+ const now = new Date();
2347
+ const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, '');
2348
+ const amzDate = dateStamp + 'T' + now.toISOString().slice(11, 19).replace(/:/g, '') + 'Z';
2349
+ const payloadHash = createHash('sha256').update(body).digest('hex');
2350
+
2351
+ const url = new URL(endpoint + pathAndQuery);
2352
+ const canonicalUri = '/';
2353
+ const canonicalQueryString = 'website=';
2354
+ const canonicalHeaders = `host:${url.host}\n` + `x-amz-content-sha256:${payloadHash}\n` + `x-amz-date:${amzDate}\n`;
2355
+ const signedHeaders = 'host;x-amz-content-sha256;x-amz-date';
2356
+ const canonicalRequest = [
2357
+ method,
2358
+ canonicalUri,
2359
+ canonicalQueryString,
2360
+ canonicalHeaders,
2361
+ signedHeaders,
2362
+ payloadHash,
2363
+ ].join('\n');
2364
+
2365
+ const credentialScope = `${dateStamp}/${region}/s3/aws4_request`;
2366
+ const stringToSign = [
2367
+ 'AWS4-HMAC-SHA256',
2368
+ amzDate,
2369
+ credentialScope,
2370
+ createHash('sha256').update(canonicalRequest).digest('hex'),
2371
+ ].join('\n');
2372
+
2373
+ const kDate = createHmac('sha256', 'AWS4' + creds.sk)
2374
+ .update(dateStamp)
2375
+ .digest();
2376
+ const kRegion = createHmac('sha256', kDate).update(region).digest();
2377
+ const kService = createHmac('sha256', kRegion).update('s3').digest();
2378
+ const kSigning = createHmac('sha256', kService).update('aws4_request').digest();
2379
+ const signature = createHmac('sha256', kSigning).update(stringToSign).digest('hex');
2380
+
2381
+ const authorization =
2382
+ `AWS4-HMAC-SHA256 Credential=${creds.ak}/${credentialScope}, ` +
2383
+ `SignedHeaders=${signedHeaders}, Signature=${signature}`;
2384
+
2385
+ const headers = {
2386
+ Host: url.host,
2387
+ 'x-amz-content-sha256': payloadHash,
2388
+ 'x-amz-date': amzDate,
2389
+ Authorization: authorization,
2390
+ };
2391
+ if (body) headers['Content-Type'] = 'application/xml';
2392
+ if (creds.securityToken) headers['x-amz-security-token'] = creds.securityToken;
2393
+
2394
+ const res = await fetchWithProxy(endpoint + pathAndQuery, {
2395
+ method,
2396
+ headers,
2397
+ body: body || undefined,
2398
+ });
2399
+ const resBody = await res.text();
2400
+ return { status: res.status, body: resBody };
2401
+ }
@@ -114,6 +114,10 @@ export function judgeUpdate(current, distTags, skipState, now = Date.now()) {
114
114
  if (!distTags) {
115
115
  return buildResult(current, null, { result: 'check_failed', note: '检测失败,不影响使用' });
116
116
  }
117
+ // truthy 但无有效版本(如 failure 折叠后的 { latest:null, next:null })同样视为检测失败
118
+ if (!distTags.latest && !distTags.next) {
119
+ return buildResult(current, null, { result: 'check_failed', note: '检测失败,不影响使用' });
120
+ }
117
121
  const target = determineTarget(current, distTags);
118
122
  if (!target || semverCompare(target, current) <= 0) {
119
123
  return buildResult(current, distTags, { result: 'up_to_date', target: target ?? null });
@@ -348,9 +352,9 @@ export function peekCachedUpdateInfo() {
348
352
  return lastHint && lastHint.updateAvailable && lastHint.targetVersion ? lastHint : null;
349
353
  }
350
354
 
351
- export async function getUpdateDistTags(current, { sessionId = null } = {}) {
352
- const result = await getCachedUpdateInfo(current, { sessionId });
353
- if (!result) return null;
355
+ export async function getUpdateDistTags(current, { sessionId = null, doQuery } = {}) {
356
+ const result = await getCachedUpdateInfo(current, { sessionId, doQuery });
357
+ if (!result || result.result === 'check_failed') return null;
354
358
  return { latest: result.latestStable ?? null, next: result.latestNext ?? null };
355
359
  }
356
360