// 生成 CodeArts 华为云签名派生的差分期望值(供 internal/core/codearts_sign_test.go 比对)。 // // 为什么需要:channels.lua 里的华为云 SDK-HMAC-SHA256 签名是**手工移植** // (Lua 没有 webcrypto)。签名错一处,服务端只回一句 // `{"error_code":"APIG.0301","error_msg":"...verify ak sk signature fail"}`, // 看起来像"账号/权限问题",极难定位。所以拿参考实现生成期望值做逐字节差分。 // // 参考真值:vendor/channel-pack/src/sign.ts 的 signRequestHuawei。 // 这里**逐行照抄**那个函数(而非 import)—— 它依赖参考仓库的构建链, // 抄这 40 行更能保证"同一算法"。 // // 用法(改了参考或改了移植后都要重跑): // node tools/codearts_sig_expected.mjs > internal/core/codearts_sig_expected.json import { createHash, createHmac } from 'node:crypto' const sha256Hex = (d) => createHash('sha256').update(d).digest('hex') const hmacSha256Hex = (key, data) => createHmac('sha256', key).update(data).digest('hex') /** 照抄 sign.ts:signRequestHuawei(dateStamp 提为参数以便固定时间)。 */ function signRequestHuawei(ak, sk, securityToken, method, urlStr, body, dateStamp) { const url = new URL(urlStr) let uri = url.pathname if (!uri.endsWith('/')) uri += '/' const query = url.search.slice(1) const payloadHash = sha256Hex(body) const headers = new Map() headers.set('host', url.host) headers.set('x-sdk-date', dateStamp) headers.set('x-sdk-content-sha256', payloadHash) headers.set('x-security-token', securityToken) if (method.toUpperCase() !== 'GET') headers.set('content-type', 'application/json') const signedHeaders = [] headers.forEach((_, k) => signedHeaders.push(k)) signedHeaders.sort() const headerLines = signedHeaders.map((k) => `${k}:${headers.get(k) ?? ''}`) const canonical = [ method, uri, query, headerLines.join('\n'), '', signedHeaders.join(';'), payloadHash, ].join('\n') const stringToSign = `SDK-HMAC-SHA256\n${dateStamp}\n${sha256Hex(new TextEncoder().encode(canonical))}` const signature = hmacSha256Hex( new TextEncoder().encode(sk), new TextEncoder().encode(stringToSign), ) return { canonical, stringToSign, signature, signedHeaders: signedHeaders.join(';'), } } // 覆盖三条真实路径(账户查询 / 活动列表带 query / 领取带 body), // 以及有/无 security_token 两种形态。 const cases = [ { m: 'GET', u: 'https://snap-access.cn-north-4.myhuaweicloud.com/snap-manager/v1/statistics/plugin', b: '', ak: 'AK1', sk: 'SK1', st: 'TOK1', }, { m: 'GET', u: 'https://snap-access.cn-north-4.myhuaweicloud.com/v1/ops/delivery?channel=IDE', b: '', ak: 'AK2', sk: 'SK2', st: '', }, { m: 'POST', u: 'https://snap-access.cn-north-4.myhuaweicloud.com/v1/ops/claim', b: '{"campaignId":"C1","channel":"IDE"}', ak: 'AK3', sk: 'SK3', st: 'T3', }, ] const out = cases.map((c) => { const r = signRequestHuawei( c.ak, c.sk, c.st, c.m, c.u, Buffer.from(c.b, 'utf8'), '20261013T150405Z', ) return { method: c.m, url: c.u, body: c.b, ak: c.ak, sk: c.sk, st: c.st, canonical: r.canonical, string_to_sign: r.stringToSign, signature: r.signature, signed_headers: r.signedHeaders, } }) process.stdout.write(`${JSON.stringify(out, null, 1)}\n`)