specific-ai-cli 0.0.0-stage → 0.1.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 (277) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +177 -0
  3. package/README.ja.md +177 -0
  4. package/README.ko.md +177 -0
  5. package/README.md +174 -2
  6. package/README.zh-TW.md +177 -0
  7. package/assets/banner.txt +3 -0
  8. package/dist/cli/argv.js +98 -0
  9. package/dist/cli/commands/auth.js +11 -0
  10. package/dist/cli/commands/config.js +23 -0
  11. package/dist/cli/commands/doctor.js +134 -0
  12. package/dist/cli/commands/platforms.js +24 -0
  13. package/dist/cli/commands/version.js +4 -0
  14. package/dist/cli/confirm.js +22 -0
  15. package/dist/cli/dispatch.js +440 -0
  16. package/dist/cli/help.js +223 -0
  17. package/dist/cli/hints.js +60 -0
  18. package/dist/cli/main.js +18 -0
  19. package/dist/cli/output.js +66 -0
  20. package/dist/core/auth-handlers.js +41 -0
  21. package/dist/core/auth-store.js +119 -0
  22. package/dist/core/capabilities.js +163 -0
  23. package/dist/core/config.js +121 -0
  24. package/dist/core/cookies.js +164 -0
  25. package/dist/core/errors.js +37 -0
  26. package/dist/core/files.js +89 -0
  27. package/dist/core/fsutil.js +52 -0
  28. package/dist/core/hash.js +4 -0
  29. package/dist/core/http.js +331 -0
  30. package/dist/core/image.js +280 -0
  31. package/dist/core/log.js +40 -0
  32. package/dist/core/login.js +191 -0
  33. package/dist/core/mp4.js +130 -0
  34. package/dist/core/normalize.js +207 -0
  35. package/dist/core/options.js +110 -0
  36. package/dist/core/paths.js +38 -0
  37. package/dist/core/pb.js +59 -0
  38. package/dist/core/py.js +181 -0
  39. package/dist/core/rand.js +131 -0
  40. package/dist/core/registry.js +129 -0
  41. package/dist/core/schemas.js +41 -0
  42. package/dist/core/stream.js +104 -0
  43. package/dist/core/toolkit.js +35 -0
  44. package/dist/core/vm.js +73 -0
  45. package/dist/core/vocab.js +157 -0
  46. package/dist/platforms/_shared/impaas.js +410 -0
  47. package/dist/platforms/bilibili/index.js +216 -0
  48. package/dist/platforms/bilibili/web/api.js +806 -0
  49. package/dist/platforms/bilibili/web/client.js +311 -0
  50. package/dist/platforms/bilibili/web/commands.js +783 -0
  51. package/dist/platforms/bilibili/web/ddddocr.js +342 -0
  52. package/dist/platforms/bilibili/web/gaia.js +130 -0
  53. package/dist/platforms/bilibili/web/geetest-manual.js +154 -0
  54. package/dist/platforms/bilibili/web/geetest-vision.js +232 -0
  55. package/dist/platforms/bilibili/web/geetest.js +450 -0
  56. package/dist/platforms/bilibili/web/normalize.js +234 -0
  57. package/dist/platforms/bilibili/web/profile.js +135 -0
  58. package/dist/platforms/bilibili/web/resolve.js +112 -0
  59. package/dist/platforms/bilibili/web/sign.js +223 -0
  60. package/dist/platforms/bilibili/web/upos.js +102 -0
  61. package/dist/platforms/douyin/index.js +142 -0
  62. package/dist/platforms/douyin/web/api.js +863 -0
  63. package/dist/platforms/douyin/web/client.js +413 -0
  64. package/dist/platforms/douyin/web/commands.js +712 -0
  65. package/dist/platforms/douyin/web/creator.js +601 -0
  66. package/dist/platforms/douyin/web/crypto.js +247 -0
  67. package/dist/platforms/douyin/web/dtrait.js +190 -0
  68. package/dist/platforms/douyin/web/im.js +502 -0
  69. package/dist/platforms/douyin/web/image.js +5 -0
  70. package/dist/platforms/douyin/web/live.js +225 -0
  71. package/dist/platforms/douyin/web/mssdk.js +148 -0
  72. package/dist/platforms/douyin/web/normalize.js +326 -0
  73. package/dist/platforms/douyin/web/passport.js +1247 -0
  74. package/dist/platforms/douyin/web/profile.js +221 -0
  75. package/dist/platforms/douyin/web/proto.js +26 -0
  76. package/dist/platforms/douyin/web/resolve.js +88 -0
  77. package/dist/platforms/douyin/web/sign.js +436 -0
  78. package/dist/platforms/douyin/web/tos.js +78 -0
  79. package/dist/platforms/index.js +17 -0
  80. package/dist/platforms/jd/index.js +67 -0
  81. package/dist/platforms/jd/web/aks.js +115 -0
  82. package/dist/platforms/jd/web/api.js +549 -0
  83. package/dist/platforms/jd/web/chat.js +132 -0
  84. package/dist/platforms/jd/web/client.js +470 -0
  85. package/dist/platforms/jd/web/commands.js +557 -0
  86. package/dist/platforms/jd/web/jcap/helper.js +90 -0
  87. package/dist/platforms/jd/web/jcap/host.js +107 -0
  88. package/dist/platforms/jd/web/jcap/lsd.js +300 -0
  89. package/dist/platforms/jd/web/jcap/solver.js +995 -0
  90. package/dist/platforms/jd/web/jcap.js +150 -0
  91. package/dist/platforms/jd/web/js.js +420 -0
  92. package/dist/platforms/jd/web/login.js +775 -0
  93. package/dist/platforms/jd/web/normalize.js +297 -0
  94. package/dist/platforms/jd/web/profile.js +256 -0
  95. package/dist/platforms/jd/web/util.js +175 -0
  96. package/dist/platforms/jd/web/webm.js +133 -0
  97. package/dist/platforms/kuaishou/index.js +98 -0
  98. package/dist/platforms/kuaishou/web/api.js +694 -0
  99. package/dist/platforms/kuaishou/web/captcha-crypto.js +134 -0
  100. package/dist/platforms/kuaishou/web/captcha-fp.js +180 -0
  101. package/dist/platforms/kuaishou/web/captcha.js +273 -0
  102. package/dist/platforms/kuaishou/web/client.js +487 -0
  103. package/dist/platforms/kuaishou/web/commands.js +863 -0
  104. package/dist/platforms/kuaishou/web/gap.js +144 -0
  105. package/dist/platforms/kuaishou/web/gdfp.js +376 -0
  106. package/dist/platforms/kuaishou/web/gql.js +11 -0
  107. package/dist/platforms/kuaishou/web/live.js +125 -0
  108. package/dist/platforms/kuaishou/web/normalize.js +241 -0
  109. package/dist/platforms/kuaishou/web/oracle.js +329 -0
  110. package/dist/platforms/kuaishou/web/profile.js +259 -0
  111. package/dist/platforms/kuaishou/web/resolve.js +84 -0
  112. package/dist/platforms/kuaishou/web/session.js +592 -0
  113. package/dist/platforms/kuaishou/web/sign.js +568 -0
  114. package/dist/platforms/taobao/index.js +56 -0
  115. package/dist/platforms/taobao/web/api.js +96 -0
  116. package/dist/platforms/taobao/web/client.js +102 -0
  117. package/dist/platforms/taobao/web/commands.js +172 -0
  118. package/dist/platforms/taobao/web/im.js +68 -0
  119. package/dist/platforms/taobao/web/normalize.js +103 -0
  120. package/dist/platforms/taobao/web/profile.js +85 -0
  121. package/dist/platforms/taobao/web/resolve.js +51 -0
  122. package/dist/platforms/taobao/web/sign.js +35 -0
  123. package/dist/platforms/tiktok/index.js +120 -0
  124. package/dist/platforms/tiktok/web/api.js +1234 -0
  125. package/dist/platforms/tiktok/web/client.js +362 -0
  126. package/dist/platforms/tiktok/web/commands.js +821 -0
  127. package/dist/platforms/tiktok/web/im.js +133 -0
  128. package/dist/platforms/tiktok/web/jsrun.js +126 -0
  129. package/dist/platforms/tiktok/web/normalize.js +286 -0
  130. package/dist/platforms/tiktok/web/profile.js +169 -0
  131. package/dist/platforms/tiktok/web/resolve.js +156 -0
  132. package/dist/platforms/tiktok/web/sign.js +585 -0
  133. package/dist/platforms/tiktok/web/upload.js +805 -0
  134. package/dist/platforms/tiktok/web/wire.js +490 -0
  135. package/dist/platforms/weibo/index.js +91 -0
  136. package/dist/platforms/weibo/web/api.js +206 -0
  137. package/dist/platforms/weibo/web/client.js +155 -0
  138. package/dist/platforms/weibo/web/commands.js +202 -0
  139. package/dist/platforms/weibo/web/normalize.js +137 -0
  140. package/dist/platforms/weibo/web/profile.js +246 -0
  141. package/dist/platforms/weibo/web/resolve.js +60 -0
  142. package/dist/platforms/weibo/web/sign.js +85 -0
  143. package/dist/platforms/x/index.js +107 -0
  144. package/dist/platforms/x/web/api.js +443 -0
  145. package/dist/platforms/x/web/article.js +212 -0
  146. package/dist/platforms/x/web/client.js +202 -0
  147. package/dist/platforms/x/web/commands.js +363 -0
  148. package/dist/platforms/x/web/graphql.js +108 -0
  149. package/dist/platforms/x/web/normalize.js +235 -0
  150. package/dist/platforms/x/web/profile.js +99 -0
  151. package/dist/platforms/x/web/resolve.js +44 -0
  152. package/dist/platforms/x/web/transaction.js +171 -0
  153. package/dist/platforms/xhs/index.js +101 -0
  154. package/dist/platforms/xhs/web/api.js +278 -0
  155. package/dist/platforms/xhs/web/client.js +540 -0
  156. package/dist/platforms/xhs/web/commands.js +1046 -0
  157. package/dist/platforms/xhs/web/creator-api.js +609 -0
  158. package/dist/platforms/xhs/web/creator.js +203 -0
  159. package/dist/platforms/xhs/web/js.js +194 -0
  160. package/dist/platforms/xhs/web/login.js +230 -0
  161. package/dist/platforms/xhs/web/normalize.js +166 -0
  162. package/dist/platforms/xhs/web/profile.js +127 -0
  163. package/dist/platforms/xhs/web/push.js +361 -0
  164. package/dist/platforms/xhs/web/resolve.js +69 -0
  165. package/dist/platforms/xhs/web/state.js +368 -0
  166. package/dist/platforms/xianyu/index.js +59 -0
  167. package/dist/platforms/xianyu/web/api.js +312 -0
  168. package/dist/platforms/xianyu/web/client.js +134 -0
  169. package/dist/platforms/xianyu/web/commands.js +305 -0
  170. package/dist/platforms/xianyu/web/im.js +73 -0
  171. package/dist/platforms/xianyu/web/normalize.js +123 -0
  172. package/dist/platforms/xianyu/web/profile.js +146 -0
  173. package/dist/platforms/xianyu/web/resolve.js +48 -0
  174. package/dist/platforms/xianyu/web/sign.js +79 -0
  175. package/package.json +77 -4
  176. package/static/douyin/Live.proto +194 -0
  177. package/static/douyin/Request.proto +89 -0
  178. package/static/douyin/Response.proto +46 -0
  179. package/static/douyin/acrawler_runtime/ac_vm.js +2 -0
  180. package/static/douyin/acrawler_runtime/browser_window_shape.json +1 -0
  181. package/static/douyin/acrawler_runtime/canvas_actual_exact.json +1 -0
  182. package/static/douyin/acrawler_runtime/run_ac_node.js +1005 -0
  183. package/static/douyin/acrawler_runtime/runner_window_keys_baseline.txt +240 -0
  184. package/static/douyin/challenge_profile.json +801 -0
  185. package/static/douyin/challenge_template_runner.js +217 -0
  186. package/static/douyin/dtrait_profile.json +66 -0
  187. package/static/douyin/mstoken_common_profile.json +1 -0
  188. package/static/douyin/mstoken_profile.json +214 -0
  189. package/static/jd/h5st5_env.js +750 -0
  190. package/static/jd/h5st5_lib.js +5 -0
  191. package/static/jd/h5st5_server.js +49 -0
  192. package/static/jd/jcap/env/env_core.js +512 -0
  193. package/static/jd/jcap/env/run.js +1307 -0
  194. package/static/jd/jcap/run/jcap_ujb96b.js +39 -0
  195. package/static/jd/package.json +4 -0
  196. package/static/jd/pc_tk_lib.js +1 -0
  197. package/static/jd/pc_tk_server.js +63 -0
  198. package/static/jd/summer_cryptico_runner.js +50 -0
  199. package/static/jd/webm/env/run.js +453 -0
  200. package/static/jd/webm/run/jdwebm-riskhandle.js +49 -0
  201. package/static/kuaishou/bundles/weapon/kwf-0.0.2.2cee19b4b7dec496.js +1 -0
  202. package/static/kuaishou/bundles/weapon/kws-0-0.0.1-obfuscated.4b7f3de1ab0ddcc2.js +1 -0
  203. package/static/kuaishou/bundles/weapon/kws-1-0.0.1-obfuscated.346ebdb1c78df316.js +1 -0
  204. package/static/kuaishou/bundles/weapon/kws-10-0.0.1-obfuscated.a33db41897b983af.js +1 -0
  205. package/static/kuaishou/bundles/weapon/kws-11-0.0.1-obfuscated.5e0a90af726d8a7e.js +1 -0
  206. package/static/kuaishou/bundles/weapon/kws-12-0.0.1-obfuscated.20ea29345706ec8c.js +1 -0
  207. package/static/kuaishou/bundles/weapon/kws-13-0.0.1-obfuscated.6b74e9640ff18648.js +1 -0
  208. package/static/kuaishou/bundles/weapon/kws-14-0.0.1-obfuscated.551e4429d156bf66.js +1 -0
  209. package/static/kuaishou/bundles/weapon/kws-15-0.0.1-obfuscated.8625b087aaba4a60.js +1 -0
  210. package/static/kuaishou/bundles/weapon/kws-16-0.0.1-obfuscated.d5ffa4020f3be99a.js +1 -0
  211. package/static/kuaishou/bundles/weapon/kws-17-0.0.1-obfuscated.6d13e19b22f50a80.js +1 -0
  212. package/static/kuaishou/bundles/weapon/kws-18-0.0.1-obfuscated.6d31405044166289.js +1 -0
  213. package/static/kuaishou/bundles/weapon/kws-19-0.0.1-obfuscated.a9a8d925942f19e4.js +1 -0
  214. package/static/kuaishou/bundles/weapon/kws-2-0.0.1-obfuscated.b849a136ae4a6b3e.js +1 -0
  215. package/static/kuaishou/bundles/weapon/kws-3-0.0.1-obfuscated.f9e77ebf6186e2ac.js +1 -0
  216. package/static/kuaishou/bundles/weapon/kws-4-0.0.1-obfuscated.2c3c155aa52ceb0e.js +1 -0
  217. package/static/kuaishou/bundles/weapon/kws-5-0.0.1-obfuscated.4e553d17691b3c5a.js +1 -0
  218. package/static/kuaishou/bundles/weapon/kws-6-0.0.1-obfuscated.f73ddd3ff13e1217.js +1 -0
  219. package/static/kuaishou/bundles/weapon/kws-7-0.0.1-obfuscated.1628b8729beaab7e.js +1 -0
  220. package/static/kuaishou/bundles/weapon/kws-8-0.0.1-obfuscated.6da6baef7aa81fff.js +1 -0
  221. package/static/kuaishou/bundles/weapon/kws-9-0.0.1-obfuscated.8645d55e5323b743.js +1 -0
  222. package/static/kuaishou/fixtures/current_follow_script_813.network-response +2 -0
  223. package/static/kuaishou/fixtures/live_ws_maps.json +61 -0
  224. package/static/kuaishou/fixtures/live_ws_proto.json +1342 -0
  225. package/static/kuaishou/js/cp-kwf.js +1 -0
  226. package/static/kuaishou/tools/kwf_oracle.js +799 -0
  227. package/static/kuaishou/tools/kws_oracle.js +885 -0
  228. package/static/kuaishou/tools/like_token_oracle.js +70 -0
  229. package/static/taobao/taobao_js_20260407.js +570 -0
  230. package/static/tiktok/Tiktok_Request.json +597 -0
  231. package/static/tiktok/package.json +3 -0
  232. package/static/tiktok/shop_bsid/core.js +1 -0
  233. package/static/tiktok/shop_bsid/env_core.js +512 -0
  234. package/static/tiktok/shop_bsid/loader.js +1 -0
  235. package/static/tiktok/shop_bsid/run.js +753 -0
  236. package/static/tiktok/shop_bsid/sign.js +100 -0
  237. package/static/tiktok/shop_bsid/webmssdk.js +1 -0
  238. package/static/tiktok/shop_bsid/webmssdk_ex.js +1 -0
  239. package/static/tiktok/signing/env/env_core.js +512 -0
  240. package/static/tiktok/signing/env/sign.js +455 -0
  241. package/static/tiktok/signing/sdk_config.json +230 -0
  242. package/static/tiktok/signing/source/webmssdk.js +1 -0
  243. package/static/tiktok/signing/source/webmssdk_ex.js +1 -0
  244. package/static/tiktok/signing/source/webmssdk_legacy.js +1 -0
  245. package/static/x/graphql.json +7777 -0
  246. package/static/x/transaction_l1.json +12 -0
  247. package/static/xhs/package.json +3 -0
  248. package/static/xhs/xhs_core/js/b1.js +210 -0
  249. package/static/xhs/xhs_core/js/mns.js +340 -0
  250. package/static/xhs/xhs_core/js/mns_0101_keystream.json +5 -0
  251. package/static/xhs/xhs_core/js/mns_keystreams.json +1 -0
  252. package/static/xhs/xhs_core/js/sign.js +225 -0
  253. package/static/xhs/xhs_core/js/websectiga_cli.js +220 -0
  254. package/static/xhs/xhs_core/js/websectiga_env.js +512 -0
  255. package/static/xhs/xhs_core/js/xs_common.js +79 -0
  256. package/static/xhs/xhs_creator/js/profile.js +115 -0
  257. package/static/xhs/xhs_creator/js/rap_fingerprint_creator.json +6 -0
  258. package/static/xhs/xhs_creator/js/reference_profile.json +282 -0
  259. package/static/xhs/xhs_creator/js/xhs_creator_sign.js +29 -0
  260. package/static/xhs/xhs_creator/js/xhs_creator_signature.js +23 -0
  261. package/static/xhs/xhs_pc/js/aes_decrypt.js +22 -0
  262. package/static/xhs/xhs_pc/js/aes_encrypt.js +30 -0
  263. package/static/xhs/xhs_pc/js/b1.js +9 -0
  264. package/static/xhs/xhs_pc/js/deflate.js +320 -0
  265. package/static/xhs/xhs_pc/js/mns.js +4 -0
  266. package/static/xhs/xhs_pc/js/profile.js +141 -0
  267. package/static/xhs/xhs_pc/js/rap.js +173 -0
  268. package/static/xhs/xhs_pc/js/rap_cli.js +30 -0
  269. package/static/xhs/xhs_pc/js/rap_crypto.js +73 -0
  270. package/static/xhs/xhs_pc/js/rap_fingerprint_template.json +8 -0
  271. package/static/xhs/xhs_pc/js/reference_profile.json +262 -0
  272. package/static/xhs/xhs_pc/js/sign.js +6 -0
  273. package/static/xhs/xhs_pc/js/web_ssk.js +82 -0
  274. package/static/xhs/xhs_pc/js/xs_common.js +4 -0
  275. package/static/xianyu/et_f.js +2 -0
  276. package/static/xianyu/gen_tfstk.js +410 -0
  277. package/static/xianyu/goofish_js_version_2.js +568 -0
@@ -0,0 +1,585 @@
1
+ import { createDecipheriv, createHash, createHmac, createPrivateKey, createPublicKey, pbkdf2Sync, sign as ecSign } from 'node:crypto';
2
+ import { hmacSha256Hex, md5Hex, sha256Hex } from '../../../core/hash.js';
3
+ import { compactJson, quote } from '../../../core/py.js';
4
+ import * as rand from '../../../core/rand.js';
5
+ /**
6
+ * TikTok Web 的纯算签名(上游 signing/pure.py、aws_v4.py、ticket_guard.py、builder/signer.py)。
7
+ * 与上游逐字节一致:对拍测试用同样的输入比较输出。
8
+ */
9
+ const GNARLY_ALPHABET = 'u09tbS3UvgDEe6r-ZVMXzLpsAohTn7mdINQlW412GqBjfYiyk8JORCF5/xKHwacP=';
10
+ const BOGUS_ALPHABET = 'Dkdpgh4ZKsQB80/Mfvw36XI1R25-WUAlEi7NLboqYTOPuzmFjJnryx9HVGcaStCe=';
11
+ const STD_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
12
+ const SIGMA = [1196819126, 600974999, 3863347763, 1451689750];
13
+ const DYNOSAUR_FIELD_COUNT = 25;
14
+ const u32 = (v) => v >>> 0;
15
+ const rotl = (v, c) => ((v << c) | (v >>> (32 - c))) >>> 0;
16
+ const utf8 = (s) => (typeof s === 'string' ? Buffer.from(s, 'utf8') : s);
17
+ /** `secrets.randbits(k)`(k ≤ 32),对拍时与 Python 侧的替换公式一致。 */
18
+ export function randbits(k) {
19
+ return Math.floor(rand.random() * 2 ** k);
20
+ }
21
+ function quarter(s, a, b, c, d) {
22
+ s[a] = u32(s[a] + s[b]);
23
+ s[d] = rotl(s[d] ^ s[a], 16);
24
+ s[c] = u32(s[c] + s[d]);
25
+ s[b] = rotl(s[b] ^ s[c], 12);
26
+ s[a] = u32(s[a] + s[b]);
27
+ s[d] = rotl(s[d] ^ s[a], 8);
28
+ s[c] = u32(s[c] + s[d]);
29
+ s[b] = rotl(s[b] ^ s[c], 7);
30
+ }
31
+ /** WebMssdk 的一个块:对角轮的调度不是标准 ChaCha。 */
32
+ function gnarlyBlock(initial, rounds) {
33
+ const s = [...initial];
34
+ let count = 0;
35
+ while (count < rounds) {
36
+ quarter(s, 0, 4, 8, 12);
37
+ quarter(s, 1, 5, 9, 13);
38
+ quarter(s, 2, 6, 10, 14);
39
+ quarter(s, 3, 7, 11, 15);
40
+ count++;
41
+ if (count >= rounds)
42
+ break;
43
+ quarter(s, 0, 5, 10, 15);
44
+ quarter(s, 1, 6, 11, 12);
45
+ quarter(s, 2, 7, 12, 13);
46
+ quarter(s, 3, 4, 13, 14);
47
+ count++;
48
+ }
49
+ return s.map((v, i) => u32(v + initial[i]));
50
+ }
51
+ function gnarlyXor(data, keyWords, rounds) {
52
+ if (keyWords.length !== 12)
53
+ throw new Error('X-Gnarly needs 12 key words');
54
+ const state = [...SIGMA, ...keyWords.map(u32)];
55
+ const out = Uint8Array.from(data);
56
+ for (let offset = 0; offset < out.length; offset += 64) {
57
+ const stream = gnarlyBlock(state, rounds);
58
+ const limit = Math.min(64, out.length - offset);
59
+ for (let i = 0; i < limit; i++)
60
+ out[offset + i] ^= (stream[i >> 2] >>> (8 * (i % 4))) & 0xff;
61
+ state[12] = u32(state[12] + 1);
62
+ }
63
+ return out;
64
+ }
65
+ function customB64(data, alphabet) {
66
+ let out = '';
67
+ for (let o = 0; o < data.length; o += 3) {
68
+ const a = data[o];
69
+ const b = o + 1 < data.length ? data[o + 1] : 0;
70
+ const c = o + 2 < data.length ? data[o + 2] : 0;
71
+ out += alphabet[a >> 2];
72
+ out += alphabet[((a & 3) << 4) | (b >> 4)];
73
+ out += o + 1 < data.length ? alphabet[((b & 15) << 2) | (c >> 6)] : alphabet[64];
74
+ out += o + 2 < data.length ? alphabet[c & 63] : alphabet[64];
75
+ }
76
+ return out;
77
+ }
78
+ /** 5.3.x 的 FNV 式 query / UA 绑定。 */
79
+ export function hashUrlState(value) {
80
+ let state = 2166136260;
81
+ for (const byte of utf8(value)) {
82
+ const mixed = u32(Math.imul(state ^ byte, 16777619));
83
+ state = u32(mixed + u32(mixed * 32));
84
+ }
85
+ return state;
86
+ }
87
+ function encodeEnvBytes(value, variant) {
88
+ const [xorBase, addBase, preXor, rotate, postAdd] = variant === 'a' ? [103, 1, null, 2, 1] : [102, 0, 165, 1, 0];
89
+ const chars = Array.from(String(value));
90
+ const length = chars.length;
91
+ const size = Math.max(length + 2, 6);
92
+ const out = new Uint8Array(size);
93
+ chars.forEach((ch, i) => {
94
+ let v = u32(ch.codePointAt(0) ^ (xorBase + i));
95
+ v = u32(v + addBase + (170 & i)) % 256;
96
+ if (preXor != null)
97
+ v ^= preXor;
98
+ v = ((v << rotate) | (v >> (8 - rotate))) & 0xff;
99
+ v = ((v ^ 187) + postAdd) & 0xff;
100
+ out[i] = v;
101
+ });
102
+ for (let i = length; i < size - 2; i++)
103
+ out[i] = (221 + i) & 0xff;
104
+ out[size - 2] = 0;
105
+ out[size - 1] = length & 0xff;
106
+ return out;
107
+ }
108
+ function rawU32(value) {
109
+ const b = Buffer.alloc(4);
110
+ b.writeUInt32BE(u32(value));
111
+ return b;
112
+ }
113
+ function dynosaurPayload(p) {
114
+ const mix = u32(((((p.ts >>> 16) & 0xffff) ^ ((p.randB >>> 16) & 0xffff)) ^ ((p.ts & 0xffff) ^ (p.randB & 0xffff))) | (p.envcode << 16));
115
+ const a = (v) => encodeEnvBytes(v, 'a');
116
+ const fields = [
117
+ a('0'),
118
+ encodeEnvBytes('1', 'b'),
119
+ encodeEnvBytes('1', 'b'),
120
+ a('0'),
121
+ a(String(mix)),
122
+ a(String(p.imageRatio)),
123
+ a(String(p.envcode)),
124
+ a(String(p.ts)),
125
+ a(p.runtimeField8),
126
+ a('0'),
127
+ a(p.payloadVersion),
128
+ rawU32(hashUrlState('')),
129
+ a(String(p.canvas)),
130
+ a('0'),
131
+ rawU32(hashUrlState(p.query)),
132
+ a(String(p.textRatio)),
133
+ rawU32(hashUrlState(p.userAgent)),
134
+ a(p.sdkVersion),
135
+ a(p.runtimeField18),
136
+ a(p.runtimeField19),
137
+ a(String(p.randB)),
138
+ a(p.page),
139
+ a(String(p.ubcode)),
140
+ a('0'),
141
+ rawU32(hashUrlState('')),
142
+ ];
143
+ let checksum = 0;
144
+ for (const f of fields)
145
+ checksum ^= f[1];
146
+ fields[0] = encodeEnvBytes(String(checksum), 'b');
147
+ const out = [];
148
+ fields.forEach((f, i) => {
149
+ if (f.length > 255)
150
+ throw new Error('bytes must be in range(0, 256)');
151
+ out.push(0x20 + i, 0, f.length, ...f);
152
+ });
153
+ if (fields.length !== DYNOSAUR_FIELD_COUNT)
154
+ throw new Error('unexpected Dynosaur field count');
155
+ return Uint8Array.from(out);
156
+ }
157
+ function intBytes(value, width) {
158
+ const out = [];
159
+ for (let i = width - 1; i >= 0; i--)
160
+ out.push(Math.floor(value / 256 ** i) % 256);
161
+ return out;
162
+ }
163
+ function encodeField(key, value, width) {
164
+ if (typeof value === 'number' && (value < 0 || value > 0xffffffff))
165
+ throw new Error('signature integer is outside uint32');
166
+ const encoded = typeof value === 'number' ? intBytes(value, width(value)) : [...utf8(value)];
167
+ if (encoded.length > 0xffff)
168
+ throw new Error('signature field is too long');
169
+ return [key & 0xff, (encoded.length >> 8) & 0xff, encoded.length & 0xff, ...encoded];
170
+ }
171
+ function xorHeader(fields) {
172
+ let x = 0;
173
+ for (const [k, v] of fields)
174
+ if (k !== 0 && typeof v === 'number')
175
+ x = u32(x ^ v);
176
+ return x;
177
+ }
178
+ /** project-post 代:整数小于 255*255 用 2 字节,否则 4 字节;键按数字排序。 */
179
+ function legacyPayload(input) {
180
+ const values = new Map(input);
181
+ values.set(0, xorHeader(values));
182
+ const out = [values.size];
183
+ for (const key of [...values.keys()].sort((a, b) => a - b))
184
+ out.push(...encodeField(key, values.get(key), (v) => (v < 255 * 255 ? 2 : 4)));
185
+ return Uint8Array.from(out);
186
+ }
187
+ const CURRENT_FIELD_ORDER = [10, 15, 3, 5, 7, 8, 16, 9, 0, 4, 11, 13, 6, 14, 12, 2, 1];
188
+ /** 5.3.x 代:整数不超过 uint16 用 2 字节;按浏览器观测到的插入顺序。 */
189
+ function currentPayload(input) {
190
+ const values = new Map(input);
191
+ values.set(0, xorHeader(values));
192
+ const order = CURRENT_FIELD_ORDER.filter((k) => values.has(k));
193
+ if (order.length !== values.size)
194
+ throw new Error('unknown current X-Gnarly fields');
195
+ const out = [order.length];
196
+ for (const key of order)
197
+ out.push(...encodeField(key, values.get(key), (v) => (v <= 0xffff ? 2 : 4)));
198
+ return Uint8Array.from(out);
199
+ }
200
+ function newKey() {
201
+ return Array.from({ length: 12 }, () => randbits(32));
202
+ }
203
+ /** 明文加密后插入 12 个密钥字,再做自定义 base64(Gnarly / Dynosaur 共用)。 */
204
+ function seal(plaintext, keyWords) {
205
+ const words = (keyWords ?? newKey()).map(u32);
206
+ const rounds = (words.reduce((s, w) => s + (w & 15), 0) & 15) + 5;
207
+ const encrypted = gnarlyXor(plaintext, words, rounds);
208
+ const keyBytes = Buffer.alloc(48);
209
+ words.forEach((w, i) => keyBytes.writeUInt32LE(w, i * 4));
210
+ let insertion = 0;
211
+ for (const v of keyBytes)
212
+ insertion = (insertion + v) % (encrypted.length + 1);
213
+ for (const v of encrypted)
214
+ insertion = (insertion + v) % (encrypted.length + 1);
215
+ const raw = Buffer.concat([Buffer.from([75]), encrypted.subarray(0, insertion), keyBytes, encrypted.subarray(insertion)]);
216
+ return customB64(raw, GNARLY_ALPHABET);
217
+ }
218
+ /** Creator Studio project/post 的 10 字段 X-Gnarly。 */
219
+ export function encodeGnarlyProject(query, body, userAgent, o = {}) {
220
+ const fields = new Map([
221
+ [1, 1],
222
+ [2, o.ubcode ?? 136],
223
+ [3, md5Hex(query)],
224
+ [4, md5Hex(body)],
225
+ [5, md5Hex(userAgent)],
226
+ [6, o.timestamp ?? rand.nowSeconds()],
227
+ [7, o.canvas ?? 2894886431],
228
+ [8, (o.timestampMs ?? rand.now()) % 0x80000000],
229
+ [9, o.sdkVersion ?? '5.1.0'],
230
+ ]);
231
+ return seal(legacyPayload(fields), o.keyWords);
232
+ }
233
+ /** 当前 Web API 的 17 字段 X-Gnarly。 */
234
+ export function encodeGnarlyCurrent(query, body, userAgent, o = {}) {
235
+ const timestamp = o.timestamp ?? rand.nowSeconds();
236
+ const timestampMs = o.timestampMs ?? rand.now();
237
+ const low16 = o.randomLow16 ?? randbits(16);
238
+ const randomA = o.random32 ?? randbits(32);
239
+ const randomB = o.randomTail ?? randbits(32);
240
+ const envcode = o.envcode ?? 65;
241
+ if (low16 < 0 || low16 > 0xffff)
242
+ throw new Error('random_low16 is outside uint16');
243
+ const fields = new Map([
244
+ [1, envcode],
245
+ [2, o.ubcode ?? 14],
246
+ [3, md5Hex(query)],
247
+ [4, md5Hex(body)],
248
+ [5, md5Hex(userAgent)],
249
+ [6, timestamp],
250
+ [7, o.canvas ?? 2894886431],
251
+ [8, timestampMs % 0x80000000],
252
+ [9, o.payloadVersion ?? '5.3.2'],
253
+ [10, o.sdkVersion ?? '1.0.0.417'],
254
+ [11, 1],
255
+ [12, o.totalRequests ?? 2],
256
+ [13, o.companionRequests ?? 3],
257
+ [14, u32((envcode << 16) | low16)],
258
+ [15, randomA],
259
+ [16, randomB],
260
+ ]);
261
+ return seal(currentPayload(fields), o.keyWords);
262
+ }
263
+ /** 当前 25 字段的 X-Dynosaur。 */
264
+ export function encodeDynosaurCurrent(query, userAgent, o = {}) {
265
+ const plaintext = dynosaurPayload({
266
+ ts: o.timestamp ?? rand.nowSeconds(),
267
+ randB: o.randB ?? rand.now() % 0x80000000,
268
+ query,
269
+ userAgent,
270
+ envcode: o.envcode ?? 65,
271
+ ubcode: o.ubcode ?? 14,
272
+ canvas: o.canvas ?? 2894886431,
273
+ page: o.page ?? '',
274
+ payloadVersion: o.payloadVersion ?? '5.3.2',
275
+ sdkVersion: o.sdkVersion ?? '1.0.0.417',
276
+ imageRatio: o.imageRatio ?? 1,
277
+ textRatio: o.textRatio ?? 2,
278
+ runtimeField8: o.runtimeField8 ?? '0',
279
+ runtimeField18: o.runtimeField18 ?? '0',
280
+ runtimeField19: o.runtimeField19 ?? '0',
281
+ });
282
+ return seal(plaintext, o.keyWords);
283
+ }
284
+ function rc4(data, key) {
285
+ const box = Array.from({ length: 256 }, (_, i) => i);
286
+ let j = 0;
287
+ for (let i = 0; i < 256; i++) {
288
+ j = (j + box[i] + key[i % key.length]) % 256;
289
+ [box[i], box[j]] = [box[j], box[i]];
290
+ }
291
+ const out = new Uint8Array(data.length);
292
+ let a = 0;
293
+ j = 0;
294
+ data.forEach((v, k) => {
295
+ a = (a + 1) % 256;
296
+ j = (j + box[a]) % 256;
297
+ [box[a], box[j]] = [box[j], box[a]];
298
+ out[k] = v ^ box[(box[a] + box[j]) % 256];
299
+ });
300
+ return out;
301
+ }
302
+ function doubleMd5(value) {
303
+ return createHash('md5').update(createHash('md5').update(utf8(value)).digest()).digest('hex');
304
+ }
305
+ /** 28 位 X-Bogus(Creator project/post 与 Shop 用)。 */
306
+ export function encodeXBogus(query, userAgent, body = '', o = {}) {
307
+ const timestamp = o.timestamp ?? rand.nowSeconds();
308
+ const ubcode = o.ubcode ?? 14;
309
+ const magic = o.magic ?? 536919696;
310
+ const md5p = Buffer.from(doubleMd5(query), 'hex');
311
+ const md5d = Buffer.from(doubleMd5(body), 'hex');
312
+ const md5u = Buffer.from(md5Hex(customB64(rc4(utf8(userAgent), [0, 1, ubcode]), STD_ALPHABET)), 'hex');
313
+ const salt = [timestamp, magic, 64, 0, 1, ubcode, md5p[14], md5p[15], md5d[14], md5d[15], md5u[14], md5u[15]];
314
+ for (const shift of [24, 16, 8, 0])
315
+ salt.push(Math.floor(timestamp / 2 ** shift) & 0xff);
316
+ for (const shift of [24, 16, 8, 0])
317
+ salt.push(Math.floor(magic / 2 ** shift) & 0xff);
318
+ let checksum = 64;
319
+ for (const v of salt.slice(3))
320
+ checksum ^= v;
321
+ salt.push(checksum, 255);
322
+ const order = [3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 4, 6, 8, 10, 12, 14, 16, 18, 20];
323
+ const filtered = order.map((i) => salt[i - 1]);
324
+ const scramble = [0, 10, 1, 11, 2, 12, 3, 13, 4, 14, 5, 15, 6, 16, 7, 17, 8, 18, 9];
325
+ const encrypted = rc4(Uint8Array.from(scramble.map((i) => filtered[i])), [255]);
326
+ return customB64(Uint8Array.from([2, 255, ...encrypted]), BOGUS_ALPHABET);
327
+ }
328
+ // ================================================================ 请求签名调度(builder/signer.py)
329
+ /** 该路径需要的签名字段;有些浏览器请求本来就不签名。 */
330
+ export function requiredSignatureKeys(url) {
331
+ const path = new URL(url).pathname;
332
+ const unsigned = [
333
+ '/api/feedback/v1/newest_reply/',
334
+ '/tiktok/v1/screen_time/upload/',
335
+ '/tiktok/v1/screen_time/list/',
336
+ '/api/v1/web/project/get/ab/',
337
+ '/api/v1/user/profile/upload/',
338
+ '/tiktok/v1/creator/publish_setting/',
339
+ '/api/v1/media/get/openid/',
340
+ '/api/v1/web-cookie-privacy/config',
341
+ '/tiktok/popup/dispatch/v1',
342
+ '/tiktok/popup/display/v1/',
343
+ '/tiktok/ppf/api/eligibility/v2',
344
+ '/api/compliance/settings/',
345
+ '/tiktok/v1/compliance/guadig/settings/',
346
+ '/api/share/settings/',
347
+ '/api/im/spotlight/relation',
348
+ '/api/privacy/user/effected_count/v1',
349
+ '/aweme/v1/report/inbox/notice',
350
+ '/tiktok/v1/community_notes/intake/check',
351
+ '/tiktok/popup/callback/v1',
352
+ '/tiktok/v1/csp/pa_prompt',
353
+ '/tiktok/music/tt_to_dsp/platform/list/v1',
354
+ ];
355
+ if (unsigned.some((p) => path.includes(p)))
356
+ return [];
357
+ if (PROJECT_PATHS.some((p) => path.includes(p)))
358
+ return ['msToken', 'X-Bogus', 'X-Gnarly'];
359
+ return ['X-Dynosaur', 'msToken', 'X-Bogus', 'X-Gnarly'];
360
+ }
361
+ const PROJECT_PATHS = ['/tiktok/web/project/post/v1/', '/tiktok/web/project/post_retry/v1/', '/api/v1/video/upload/auth/', '/tiktok/creator/manage/item_list/v1/'];
362
+ export class SignerError extends Error {
363
+ }
364
+ function metricNumber(m, key, fallback) {
365
+ return m[key] == null ? fallback : Number(m[key]);
366
+ }
367
+ function metricString(m, key, fallback) {
368
+ return m[key] == null ? fallback : String(m[key]);
369
+ }
370
+ function checkLengths(values, expected, what) {
371
+ if (!expected)
372
+ return;
373
+ const bad = Object.entries(expected).filter(([k, n]) => values[k]?.length !== Number(n));
374
+ if (bad.length)
375
+ throw new SignerError(`${what} 签名长度不匹配: ${bad.map(([k]) => k).join(', ')}`);
376
+ }
377
+ /** 按路径选择签名代:Creator project 系列走 X-Bogus + 10 字段 Gnarly,其余走 Dynosaur + 17 字段 Gnarly。 */
378
+ export function signRequest(input) {
379
+ const body = input.body == null ? null : typeof input.body === 'string' ? input.body : bytesOrText(input.body);
380
+ if (['POST', 'PUT', 'PATCH'].includes(input.method.toUpperCase()) && body == null)
381
+ throw new SignerError('签名需要完整 body,不能对有 body 的方法省略 body');
382
+ const values = PROJECT_PATHS.some((p) => input.url.includes(p)) ? signProject(input, body ?? '') : signLegacy(input, body);
383
+ const required = requiredSignatureKeys(input.url);
384
+ const missing = required.filter((k) => !values[k]);
385
+ if (missing.length)
386
+ throw new SignerError(`纯计算后缺少字段: ${missing.join(', ')}`);
387
+ return Object.fromEntries(required.map((k) => [k, values[k]]));
388
+ }
389
+ /** 能按 UTF-8 解码的字节按字符串签名(与上游 body.decode('utf-8') 一致),gzip 等二进制保持字节。 */
390
+ function bytesOrText(b) {
391
+ try {
392
+ return new TextDecoder('utf-8', { fatal: true }).decode(b);
393
+ }
394
+ catch {
395
+ return b;
396
+ }
397
+ }
398
+ function signLegacy(input, body) {
399
+ const query = splitQuery(input.url);
400
+ const parts = query ? query.split('&') : [];
401
+ const tokens = parts.filter((p) => p.startsWith('msToken='));
402
+ const tokenPart = tokens[tokens.length - 1];
403
+ const token = tokenPart ? tokenPart.slice(tokenPart.indexOf('=') + 1) : '';
404
+ if (!token)
405
+ throw new SignerError('legacy Web API unsigned URL 缺少浏览器 msToken');
406
+ const base = parts.filter((p) => !['X-Dynosaur=', 'X-Bogus=', 'X-Gnarly=', 'msToken='].some((x) => p.startsWith(x))).join('&');
407
+ const ua = input.userAgent;
408
+ if (!ua)
409
+ throw new SignerError('legacy Web API 纯计算需要完整 user-agent');
410
+ const m = input.metrics ?? {};
411
+ const nowMs = metricNumber(m, 'timestamp_ms', rand.now());
412
+ const timestamp = input.signingTimestamp || Math.floor(nowMs / 1000);
413
+ const randB = m.rand_b != null ? Number(m.rand_b) : 100_000_000 + (hashUrlState(base) % 900_000_000);
414
+ let page = String(m.dynosaur_page ?? m.page ?? '');
415
+ if (!page) {
416
+ const ref = urlsplit(input.referer || 'https://www.tiktok.com/');
417
+ page = ref.netloc + (ref.path || '/');
418
+ }
419
+ let field8 = m.runtime_field8 ?? m.dynosaur_field8;
420
+ if (field8 == null) {
421
+ const digest = createHash('sha256').update(`${base}|${ua}`, 'utf8').digest();
422
+ field8 = String(1_000_000_000 + (digest.readUInt32BE(0) % 3_000_000_000));
423
+ }
424
+ const field18 = String(m.runtime_field18 ?? m.dynosaur_field18 ?? '1.0.0.2870');
425
+ const field19 = String(m.runtime_field19 ?? m.dynosaur_field19 ?? md5Hex(`${ua}|${page}`));
426
+ const envcode = metricNumber(m, 'envcode', 65);
427
+ const ubcode = metricNumber(m, 'ubcode', 14);
428
+ const canvas = metricNumber(m, 'canvas', 2894886431);
429
+ const payloadVersion = metricString(m, 'payload_version', '5.3.2');
430
+ const sdkVersion = metricString(m, 'sdk_version', '1.0.0.417');
431
+ const dynosaur = encodeDynosaurCurrent(base, ua, {
432
+ page,
433
+ envcode,
434
+ ubcode,
435
+ canvas,
436
+ timestamp,
437
+ randB,
438
+ payloadVersion,
439
+ sdkVersion,
440
+ imageRatio: metricNumber(m, 'image_ratio', 1),
441
+ textRatio: metricNumber(m, 'text_ratio', 2),
442
+ runtimeField8: String(field8),
443
+ runtimeField18: field18,
444
+ runtimeField19: field19,
445
+ });
446
+ const signedQuery = `${base}&X-Dynosaur=${dynosaur}&${tokenPart}`;
447
+ const gnarly = encodeGnarlyCurrent(signedQuery, body ?? '', ua, {
448
+ envcode,
449
+ ubcode,
450
+ canvas,
451
+ timestamp,
452
+ timestampMs: nowMs,
453
+ payloadVersion,
454
+ sdkVersion,
455
+ totalRequests: metricNumber(m, 'total_requests', 2),
456
+ companionRequests: metricNumber(m, 'companion_requests', 3),
457
+ randomLow16: m.random_low16 == null ? undefined : Number(m.random_low16),
458
+ random32: m.random32 == null ? undefined : Number(m.random32),
459
+ randomTail: m.random_tail == null ? undefined : Number(m.random_tail),
460
+ });
461
+ // 当前 Web API 的 X-Bogus 是字面量 1(端点约定,不是占位)
462
+ const values = { 'X-Dynosaur': dynosaur, msToken: token, 'X-Bogus': '1', 'X-Gnarly': gnarly };
463
+ checkLengths(values, input.expectedLengths, '纯 legacy');
464
+ return values;
465
+ }
466
+ function signProject(input, body) {
467
+ const query = splitQuery(input.url);
468
+ const token = query
469
+ .split('&')
470
+ .find((p) => p.startsWith('msToken='))
471
+ ?.split('=')
472
+ .slice(1)
473
+ .join('=');
474
+ if (!token)
475
+ throw new SignerError('project/post unsigned URL 缺少浏览器 msToken');
476
+ if (!input.userAgent)
477
+ throw new SignerError('project/post 纯计算需要完整 user-agent');
478
+ const canvas = metricNumber(input.metrics ?? {}, 'canvas', 2894886431);
479
+ const values = {
480
+ msToken: token,
481
+ 'X-Bogus': encodeXBogus(query, input.userAgent, body, { timestamp: input.signingTimestamp, ubcode: 136, magic: canvas }),
482
+ 'X-Gnarly': encodeGnarlyProject(query, body, input.userAgent, { timestamp: input.signingTimestamp, ubcode: 136, canvas }),
483
+ };
484
+ checkLengths(values, input.expectedLengths, '纯 project/post');
485
+ return values;
486
+ }
487
+ /** Python 的 `urllib.parse.urlsplit`:各部分保持原样,不解码、不规范化。 */
488
+ export function urlsplit(url) {
489
+ const m = /^(?:([a-zA-Z][a-zA-Z0-9+.-]*):)?(?:\/\/([^/?#]*))?([^?#]*)(?:\?([^#]*))?(?:#(.*))?$/.exec(url);
490
+ return { scheme: m[1] ?? '', netloc: m[2] ?? '', path: m[3] ?? '', query: m[4] ?? '', fragment: m[5] ?? '' };
491
+ }
492
+ function splitQuery(url) {
493
+ return urlsplit(url).query;
494
+ }
495
+ // ================================================================ AWS SigV4(signing/aws_v4.py)
496
+ export function canonicalAwsQuery(pairs) {
497
+ const encoded = pairs.map(([k, v]) => [quote(k, '-_.~'), quote(v, '-_.~')]);
498
+ encoded.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0));
499
+ return encoded.map(([k, v]) => `${k}=${v}`).join('&');
500
+ }
501
+ const hmac = (key, value) => createHmac('sha256', key).update(value, 'utf8').digest();
502
+ export function amzDate(ms = rand.now()) {
503
+ return new Date(ms).toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '');
504
+ }
505
+ export function signAwsV4(o) {
506
+ if (!o.accessKeyId || !o.secretAccessKey || !o.sessionToken)
507
+ throw new Error('AWS V4 缺少 access key、secret key 或 session token');
508
+ const payload = utf8(o.body ?? new Uint8Array());
509
+ const date = o.amzDate ?? amzDate();
510
+ const region = o.region ?? 'ap-singapore-1';
511
+ const service = o.service ?? 'vod';
512
+ const payloadHash = sha256Hex(payload);
513
+ const signed = ['x-amz-date', 'x-amz-security-token'];
514
+ let canonicalHeaders = `x-amz-date:${date}\nx-amz-security-token:${o.sessionToken}\n`;
515
+ if (payload.length) {
516
+ signed.unshift('x-amz-content-sha256');
517
+ canonicalHeaders = `x-amz-content-sha256:${payloadHash}\n` + canonicalHeaders;
518
+ }
519
+ const signedHeaders = signed.join(';');
520
+ const canonicalRequest = [o.method.toUpperCase(), quote(o.path, '/-_.~'), canonicalAwsQuery(o.query), canonicalHeaders, signedHeaders, payloadHash].join('\n');
521
+ const scope = `${date.slice(0, 8)}/${region}/${service}/aws4_request`;
522
+ const stringToSign = ['AWS4-HMAC-SHA256', date, scope, sha256Hex(Buffer.from(canonicalRequest, 'utf8'))].join('\n');
523
+ let key = hmac('AWS4' + o.secretAccessKey, date.slice(0, 8));
524
+ key = hmac(key, region);
525
+ key = hmac(key, service);
526
+ key = hmac(key, 'aws4_request');
527
+ const signature = hmacSha256Hex(key, stringToSign);
528
+ const headers = {
529
+ authorization: `AWS4-HMAC-SHA256 Credential=${o.accessKeyId}/${scope}, SignedHeaders=${signedHeaders}, Signature=${signature}`,
530
+ 'x-amz-security-token': o.sessionToken,
531
+ 'x-amz-date': date,
532
+ };
533
+ if (payload.length)
534
+ headers['x-amz-content-sha256'] = payloadHash;
535
+ return headers;
536
+ }
537
+ // ================================================================ tt-ticket-guard(signing/ticket_guard.py)
538
+ /** 解密浏览器 security-sdk 存的 encrypt_ticket(AES-GCM,PBKDF2 派生密钥)。 */
539
+ export function decryptEncryptTicket(encryptTicket) {
540
+ const data = Buffer.from(String(encryptTicket), 'base64');
541
+ if (data.length <= 12 + 16)
542
+ throw new Error('encrypt_ticket 长度不足,无法解密');
543
+ const key = pbkdf2Sync('tt-ticket-guard-iv', 'secure-salt', 1000, 16, 'sha256');
544
+ const decipher = createDecipheriv('aes-128-gcm', key, data.subarray(0, 12));
545
+ decipher.setAuthTag(data.subarray(data.length - 16));
546
+ const ticket = Buffer.concat([decipher.update(data.subarray(12, data.length - 16)), decipher.final()]).toString('utf8');
547
+ if (!ticket)
548
+ throw new Error('encrypt_ticket 解密为空');
549
+ return ticket;
550
+ }
551
+ /** Chrome 的 65 字节未压缩 P-256 公钥,base64。 */
552
+ export function publicKeyBase64(privateKeyPem) {
553
+ const jwk = createPublicKey(createPrivateKey(privateKeyPem)).export({ format: 'jwk' });
554
+ if (jwk.kty !== 'EC' || !jwk.x || !jwk.y)
555
+ throw new Error('ticket guard private key 不是 EC 私钥');
556
+ return Buffer.concat([Buffer.from([4]), Buffer.from(jwk.x, 'base64url'), Buffer.from(jwk.y, 'base64url')]).toString('base64');
557
+ }
558
+ export function signTicketPath(privateKeyPem, ticket, path, timestamp) {
559
+ const data = `ticket=${ticket}&path=${path}&timestamp=${Math.trunc(timestamp)}`;
560
+ return ecSign('sha256', Buffer.from(data, 'utf8'), { key: createPrivateKey(privateKeyPem), dsaEncoding: 'der' }).toString('base64');
561
+ }
562
+ export function encodeClientData(o) {
563
+ const payload = { ts_sign: o.tsSign, req_content: o.reqContent ?? 'ticket,path,timestamp', req_sign: o.reqSign, timestamp: Math.trunc(o.timestamp) };
564
+ return Buffer.from(compactJson(payload), 'utf8').toString('base64');
565
+ }
566
+ /** 五个 tt-ticket-guard 请求头,每次用浏览器私钥现签。 */
567
+ export function ticketGuardHeaders(state, path, timestamp = rand.nowSeconds()) {
568
+ const ticket = decryptEncryptTicket(state.encryptTicket);
569
+ const reqSign = signTicketPath(state.privateKey, ticket, path, timestamp);
570
+ return {
571
+ 'tt-ticket-guard-public-key': publicKeyBase64(state.privateKey),
572
+ 'tt-ticket-guard-web-version': '1',
573
+ 'tt-ticket-guard-version': String(state.version || '2'),
574
+ 'tt-ticket-guard-iteration-version': String(state.iterationVersion || '0'),
575
+ 'tt-ticket-guard-client-data': encodeClientData({ tsSign: state.tsSign, reqSign, timestamp }),
576
+ };
577
+ }
578
+ /** client-data 里的时间戳(project/post 用它作签名时间)。 */
579
+ export function ticketTimestamp(clientData) {
580
+ const payload = JSON.parse(Buffer.from(clientData.trim(), 'base64url').toString('utf8'));
581
+ const value = Number(payload.timestamp);
582
+ if (!(value > 0))
583
+ throw new Error('ticket-guard-client-data timestamp 无效');
584
+ return Math.trunc(value);
585
+ }