@ikenxuan/amagi 6.6.0 → 7.0.0-beta.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.
@@ -0,0 +1,168 @@
1
+ import { $ as unwrap, A as douyinFetcher, B as xiaohongshuSign, C as ValidationError, Ct as parseDmSegMobileReply, D as createBoundKuaishouFetcher, E as xiaohongshuFetcher, F as bilibiliFetcher, G as createDouyinRoutes, H as createKuaishouRoutes, I as createBoundBilibiliFetcher, J as bilibiliUtils, K as passport_exports, L as createClient, M as requestPassportQrcode, N as sendPassportVerifyCode, O as kuaishouFetcher, P as validatePassportVerifyCode, Q as isSuccess, R as xiaohongshuUtils, S as ApiError, St as emitNetworkRetry, T as createBoundXiaohongshuFetcher, Tt as bv2av, U as kuaishouSign, V as kuaishouUtils, W as douyinUtils, X as AmagiThrownError, Y as createBilibiliRoutes, Z as isFailure, _ as validateBilibiliParams, _t as emitLogError, a as AdditionalType, at as wbi_sign, b as validateXiaohongshuParams, bt as emitLogWarn, c as createAmagiClient, ct as fetchResponse, d as assertValidBilibiliParams, dt as emitApiError, et as AMAGI_BUS_EVENT_NAMES, f as assertValidDouyinParams, ft as emitApiSuccess, g as createSuccessResponse, gt as emitLogDebug, h as createErrorResponse, ht as emitLog, i as DynamicType, it as qtparam, j as checkPassportQrcode, k as createBoundDouyinFetcher, l as kuaishouApiUrls, lt as isNetworkErrorResult, m as assertValidXiaohongshuParams, mt as emitHttpResponse, n as CreateApp, nt as KUAISHOU_PC_CAPTCHA_RESULT, o as MajorType, ot as bilibiliApiUrls, p as assertValidKuaishouParams, pt as emitHttpRequest, q as douyinSign, r as amagi, rt as parseKuaishouCaptcha, s as CommentType, st as fetchData, tt as KUAISHOU_H5_CAPTCHA_RESULT, u as douyinApiUrls, ut as amagiEvents, v as validateDouyinParams, vt as emitLogInfo, w as handleError, wt as av2bv, x as xiaohongshuApiUrls, xt as emitNetworkError, y as validateKuaishouParams, yt as emitLogMark, z as createXiaohongshuRoutes } from "../src-AA7Elfxn.mjs";
2
+ //#region src/exports/compat.ts
3
+ /** 模块级只执行一次的迁移提示(不刷屏) */
4
+ emitLogWarn("[@ikenxuan/amagi/compat] 你正在使用 v6 兼容入口;v7 迁移说明见 https://github.com/ikenxuan/amagi (v8 将移除 compat)");
5
+ /**
6
+ * `ErrorKind` → v6 信封顶层 `code`。
7
+ *
8
+ * v6 信封顶层 `code` 的语义并不统一(ApiError code 直接当 HTTP 状态、
9
+ * 平台码与 amagi 错误码混用),因此这里是近似而非逐字还原:取
10
+ * kind → HTTP 语义码的稳定映射,平台原文码保留在 `error.code`。
11
+ */
12
+ const KIND_LEGACY_CODE = {
13
+ validation: 400,
14
+ auth: 401,
15
+ forbidden: 403,
16
+ not_found: 404,
17
+ rate_limit: 429,
18
+ risk: 403,
19
+ unavailable: 503,
20
+ network: 500,
21
+ timeout: 504,
22
+ parse: 502,
23
+ internal: 500,
24
+ unknown: 500
25
+ };
26
+ /**
27
+ * 把 v7 信封回填成 v6 信封(纯函数,不抛错)。
28
+ *
29
+ * - 成功:`code: 200`,`error` 键运行时补 `undefined`
30
+ * - 失败:顶层 `code` 取 {@link KIND_LEGACY_CODE};平台码进 `error.code`;
31
+ * 原始响应体进 `error.data`(没有则 null)
32
+ */
33
+ const toLegacy = (r) => {
34
+ if (r.success) return {
35
+ success: true,
36
+ code: 200,
37
+ message: r.message,
38
+ data: r.data,
39
+ error: void 0
40
+ };
41
+ const e = r.error;
42
+ const requestType = r.meta.endpoint.includes(".") ? r.meta.endpoint.split(".").slice(1).join(".") : r.meta.endpoint;
43
+ return {
44
+ success: false,
45
+ code: KIND_LEGACY_CODE[e.kind],
46
+ message: r.message,
47
+ data: void 0,
48
+ error: {
49
+ code: e.platform?.code ?? e.code,
50
+ data: e.raw ?? null,
51
+ amagiError: {
52
+ errorDescription: e.message,
53
+ requestType,
54
+ requestUrl: void 0
55
+ },
56
+ amagiMessage: e.message
57
+ }
58
+ };
59
+ };
60
+ /** 是不是 v6 信封(顶层带 `code`)。passport 等保留方法返回 v6 信封,直接透传 */
61
+ const isLegacyEnvelope = (r) => typeof r === "object" && r !== null && "success" in r && "code" in r;
62
+ /** `kind: 'validation'` 的失败信封 → v6 的抛出行为(抛 ValidationError) */
63
+ const validationErrorOf = (e) => {
64
+ const errors = e.issues && e.issues.length > 0 ? e.issues.map((i) => ({
65
+ field: i.path,
66
+ message: i.message
67
+ })) : [{
68
+ field: "(endpoint)",
69
+ message: e.message
70
+ }];
71
+ return new ValidationError("参数验证失败", errors);
72
+ };
73
+ /** 一次调用的结果收口:v6 信封透传;v7 信封转 toLegacy;校验失败恢复抛出 */
74
+ const settle = async (promise) => {
75
+ const out = await promise;
76
+ if (isLegacyEnvelope(out)) return out;
77
+ const result = out;
78
+ if (!result.success && result.error.kind === "validation") throw validationErrorOf(result.error);
79
+ return toLegacy(result);
80
+ };
81
+ const WRAP_CACHE = /* @__PURE__ */ new WeakMap();
82
+ /**
83
+ * 把一个 fetcher 对象包装成「结果转 v6 信封」的版本。
84
+ *
85
+ * 包装发生在 get 时,方法集合自动跟随原对象;每个方法只包一次并缓存
86
+ * (WeakMap 按目标对象 + 键)。非函数属性(若存在)原样透传。
87
+ */
88
+ const wrapFetcher = (fetcher) => {
89
+ return new Proxy(fetcher, { get(target, prop, receiver) {
90
+ const value = Reflect.get(target, prop, receiver);
91
+ if (typeof value !== "function" || prop === "then") return value;
92
+ let cache = WRAP_CACHE.get(target);
93
+ if (!cache) {
94
+ cache = /* @__PURE__ */ new Map();
95
+ WRAP_CACHE.set(target, cache);
96
+ }
97
+ let wrapped = cache.get(prop);
98
+ if (!wrapped) {
99
+ const fn = value;
100
+ wrapped = (...args) => settle(Promise.resolve(fn.apply(target, args)));
101
+ cache.set(prop, wrapped);
102
+ }
103
+ return wrapped;
104
+ } });
105
+ };
106
+ /** 兼容版 client 工厂:`createAmagiClient` 的每个平台 fetcher 包一层 */
107
+ const compatCreateAmagiClient = (options) => {
108
+ const client = createAmagiClient(options);
109
+ return {
110
+ ...client,
111
+ douyin: {
112
+ ...client.douyin,
113
+ douyinApiUrls,
114
+ fetcher: wrapFetcher(client.douyin.fetcher)
115
+ },
116
+ bilibili: {
117
+ ...client.bilibili,
118
+ bilibiliApiUrls,
119
+ fetcher: wrapFetcher(client.bilibili.fetcher)
120
+ },
121
+ kuaishou: {
122
+ ...client.kuaishou,
123
+ kuaishouApiUrls,
124
+ fetcher: wrapFetcher(client.kuaishou.fetcher)
125
+ },
126
+ xiaohongshu: {
127
+ ...client.xiaohongshu,
128
+ xiaohongshuApiUrls,
129
+ fetcher: wrapFetcher(client.xiaohongshu.fetcher)
130
+ }
131
+ };
132
+ };
133
+ /** 兼容版静态 fetcher */
134
+ const compatDouyinFetcher = wrapFetcher(douyinFetcher);
135
+ const compatBilibiliFetcher = wrapFetcher(bilibiliFetcher);
136
+ const compatKuaishouFetcher = wrapFetcher(kuaishouFetcher);
137
+ const compatXiaohongshuFetcher = wrapFetcher(xiaohongshuFetcher);
138
+ /** 兼容版 bound 工厂(返回的 fetcher 已包层) */
139
+ const compatCreateBoundDouyinFetcher = (cookie, requestConfig) => wrapFetcher(createBoundDouyinFetcher(cookie, requestConfig));
140
+ const compatCreateBoundBilibiliFetcher = (cookie, requestConfig) => wrapFetcher(createBoundBilibiliFetcher(cookie, requestConfig));
141
+ const compatCreateBoundKuaishouFetcher = (cookie, requestConfig) => wrapFetcher(createBoundKuaishouFetcher(cookie, requestConfig));
142
+ const compatCreateBoundXiaohongshuFetcher = (cookie, requestConfig) => wrapFetcher(createBoundXiaohongshuFetcher(cookie, requestConfig));
143
+ function CreateAmagiCompatApp(options = {}) {
144
+ return compatCreateAmagiClient(options);
145
+ }
146
+ Object.defineProperty(CreateAmagiCompatApp, "version", {
147
+ value: amagi.version,
148
+ writable: false,
149
+ enumerable: true,
150
+ configurable: false
151
+ });
152
+ CreateAmagiCompatApp.douyin = amagi.douyin;
153
+ CreateAmagiCompatApp.bilibili = amagi.bilibili;
154
+ CreateAmagiCompatApp.kuaishou = amagi.kuaishou;
155
+ CreateAmagiCompatApp.xiaohongshu = amagi.xiaohongshu;
156
+ CreateAmagiCompatApp.events = amagi.events;
157
+ CreateAmagiCompatApp.on = amagi.on;
158
+ CreateAmagiCompatApp.once = amagi.once;
159
+ CreateAmagiCompatApp.douyinFetcher = compatDouyinFetcher;
160
+ CreateAmagiCompatApp.bilibiliFetcher = compatBilibiliFetcher;
161
+ CreateAmagiCompatApp.kuaishouFetcher = compatKuaishouFetcher;
162
+ CreateAmagiCompatApp.xiaohongshuFetcher = compatXiaohongshuFetcher;
163
+ CreateAmagiCompatApp.createBoundDouyinFetcher = compatCreateBoundDouyinFetcher;
164
+ CreateAmagiCompatApp.createBoundBilibiliFetcher = compatCreateBoundBilibiliFetcher;
165
+ CreateAmagiCompatApp.createBoundKuaishouFetcher = compatCreateBoundKuaishouFetcher;
166
+ CreateAmagiCompatApp.createBoundXiaohongshuFetcher = compatCreateBoundXiaohongshuFetcher;
167
+ //#endregion
168
+ export { AMAGI_BUS_EVENT_NAMES, AdditionalType, AmagiThrownError, ApiError, CommentType, CreateAmagiCompatApp, CreateAmagiCompatApp as default, CreateApp, DynamicType, KIND_LEGACY_CODE, KUAISHOU_H5_CAPTCHA_RESULT, KUAISHOU_PC_CAPTCHA_RESULT, MajorType, ValidationError, amagi, amagiEvents, assertValidBilibiliParams, assertValidDouyinParams, assertValidKuaishouParams, assertValidXiaohongshuParams, av2bv, bilibiliApiUrls, compatBilibiliFetcher as bilibiliFetcher, compatBilibiliFetcher, bilibiliUtils, bv2av, checkPassportQrcode, compatCreateBoundBilibiliFetcher, compatCreateBoundBilibiliFetcher as createBoundBilibiliFetcher, compatCreateBoundDouyinFetcher, compatCreateBoundDouyinFetcher as createBoundDouyinFetcher, compatCreateBoundKuaishouFetcher, compatCreateBoundKuaishouFetcher as createBoundKuaishouFetcher, compatCreateBoundXiaohongshuFetcher, compatCreateBoundXiaohongshuFetcher as createBoundXiaohongshuFetcher, compatDouyinFetcher, compatDouyinFetcher as douyinFetcher, compatKuaishouFetcher, compatKuaishouFetcher as kuaishouFetcher, compatXiaohongshuFetcher, compatXiaohongshuFetcher as xiaohongshuFetcher, compatCreateAmagiClient as createAmagiClient, createBilibiliRoutes, createClient, createDouyinRoutes, createErrorResponse, createKuaishouRoutes, createSuccessResponse, createXiaohongshuRoutes, douyinApiUrls, passport_exports as douyinPassport, douyinSign, douyinUtils, emitApiError, emitApiSuccess, emitHttpRequest, emitHttpResponse, emitLog, emitLogDebug, emitLogError, emitLogInfo, emitLogMark, emitLogWarn, emitNetworkError, emitNetworkRetry, fetchData, fetchResponse, handleError, isFailure, isNetworkErrorResult, isSuccess, kuaishouApiUrls, kuaishouSign, kuaishouUtils, parseDmSegMobileReply, parseKuaishouCaptcha, qtparam, requestPassportQrcode, sendPassportVerifyCode, toLegacy, unwrap, validateBilibiliParams, validateDouyinParams, validateKuaishouParams, validatePassportVerifyCode, validateXiaohongshuParams, wbi_sign, wrapFetcher, xiaohongshuApiUrls, xiaohongshuSign, xiaohongshuUtils };
@@ -0,0 +1,437 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_tokens = require("../tokens-DHKAuPVW.cjs");
3
+ //#region src/platforms/douyin/sign/decode.ts
4
+ /**
5
+ * 把签名参数读回明文 —— 验证工具的解码层。
6
+ *
7
+ * 移植自 Douyin_TikTok_Download_API 的 `signing/native/decoding.py`(Apache-2.0)。
8
+ * 存在的理由只有一个:**证明签名实现还是对的**。
9
+ *
10
+ * ## 为什么需要它
11
+ *
12
+ * 抖音对签名是抽样校验的,所以「请求成功」证明不了任何事。唯一能证伪一个常量的办法,
13
+ * 是把真实签名拆开看。旧实现用错盐值(`cus`)活了两年没被发现,就是因为整套测试都是
14
+ * 自证的——自生成快照、和被测量对象比自身、以及没有任何一条断言能证伪一个常量。
15
+ *
16
+ * 这个模块提供外部锚点:给它一份浏览器产出的签名,它会告诉你签名里封住的到底是什么,
17
+ * 以及你手上的候选输入是不是它封的那一个。
18
+ *
19
+ * ## 「解码」在这里诚实地指三件不同的事
20
+ *
21
+ * 把它们混为一谈,就是这类工具开始撒谎的方式:
22
+ *
23
+ * - **可还原**:值就在载荷里,能原样取回。时钟、`aid`、`page_id`、屏幕几何串、
24
+ * 版本串、调用计数。没有任何推断。
25
+ * - **被绑定但不可还原**:载荷里带着某个东西的摘要——a_bogus 里是 query / body / UA
26
+ * 各三个 SM3 字节。输入无法从摘要倒推出来,所以这个模块**从不假装能**。它做的是
27
+ * **校验**:给它一个候选值,它回答这个候选是不是签名封住的那一个,以及有多少位证据
28
+ * 支持这个结论。这才是哈希真正的逆运算,而且比猜一个值有用得多——它把
29
+ * 「我的请求被拒了」变成「我发的这个签名是对着另一条 URL 算的」。
30
+ * - **根本没被计算**:`msToken` 与访客令牌由平台签发或随机抽出来,底下没有明文可找。
31
+ * 说出这一点就是答案,而不是「没能给出答案」。
32
+ *
33
+ * 每个字段都携带自己属于哪一类,读者不会被留在「标签旁边那个数字是读出来的还是推出来的」
34
+ * 这种猜测里。
35
+ *
36
+ * 这里没有任何东西用于签名。它只跑在调用方已经持有的值上,不碰网络、不消耗身份。
37
+ */
38
+ /** 字段是什么。控制台按它上色,调用方也可以对它分支 */
39
+ const KIND = {
40
+ /** 从载荷里直接读出,就是签名者放进去的东西 */
41
+ PLAIN: "plain",
42
+ /** 一个时钟,同时给出原始数字与 ISO-8601 时刻 */
43
+ TIME: "time",
44
+ /** 哈希或哈希的一个字节。输入不可还原——见 checks */
45
+ DIGEST: "digest",
46
+ /** 内部冗余。在这里重算,这正是解码得以自证的依据 */
47
+ CHECKSUM: "checksum",
48
+ /** SDK 对它自认为身处其中的页面所做的回报 */
49
+ ENVIRONMENT: "environment",
50
+ /** 每次捕获里都有、但含义未被确认的东西。命名它,而不是编造它 */
51
+ OPAQUE: "opaque"
52
+ };
53
+ /** 一个参数底下为什么没有明文 */
54
+ const REASON = {
55
+ MALFORMED: "malformed",
56
+ NOT_COMPUTED: "not_computed",
57
+ ONE_WAY: "one_way",
58
+ UNKNOWN_PARAMETER: "unknown_parameter"
59
+ };
60
+ /** 候选输入是不是签名封住的那一个 */
61
+ const CHECK = {
62
+ MATCH: "match",
63
+ DIFFERS: "differs",
64
+ NOT_SUPPLIED: "not_supplied"
65
+ };
66
+ /** 每条摘要链钉住输入的位数 */
67
+ const CHAIN_BITS = 24;
68
+ const FORTNIGHT_MS = 12096e5;
69
+ /** 毫秒时钟转 ISO-8601 UTC;数字荒诞到解析不了时退回原样,因为那通常正是它最有意思的地方 */
70
+ const instant = (milliseconds) => {
71
+ const date = new Date(milliseconds);
72
+ return Number.isNaN(date.getTime()) ? String(milliseconds) : date.toISOString().replace(".000Z", "Z");
73
+ };
74
+ const clockField = (name, milliseconds, detail) => ({
75
+ name,
76
+ value: `${milliseconds} (${instant(milliseconds)})`,
77
+ kind: KIND.TIME,
78
+ detail
79
+ });
80
+ const check = (name, expected, actual, covered) => expected.join(",") === actual.join(",") ? {
81
+ name,
82
+ status: CHECK.MATCH,
83
+ bits: CHAIN_BITS,
84
+ covered
85
+ } : {
86
+ name,
87
+ status: CHECK.DIFFERS,
88
+ bits: CHAIN_BITS
89
+ };
90
+ /**
91
+ * 拆开一份 `a_bogus`,并就调用方给出的候选值做校验。
92
+ * @param value - `a_bogus` 的值
93
+ * @param candidates - 候选的 query / body / User-Agent,给哪个校验哪个
94
+ * @returns 拆解结果;格式不合法时 `recovered` 为 `false` 并给出 `reason`
95
+ */
96
+ const decodeABogus = (value, candidates = {}) => {
97
+ const problem = require_tokens.structureError(value);
98
+ if (problem !== null) return {
99
+ parameter: "a_bogus",
100
+ platform: "douyin",
101
+ algorithm: "a_bogus",
102
+ recovered: false,
103
+ reason: `${REASON.MALFORMED}:${problem}`,
104
+ fields: [],
105
+ checks: [],
106
+ notes: []
107
+ };
108
+ const raw = require_tokens.decode(value);
109
+ const scalars = raw.fields;
110
+ const fortnights = scalars.L26;
111
+ const fields = [
112
+ {
113
+ name: "header_magic",
114
+ value: `[${raw.headerMagic.join(", ")}]`,
115
+ kind: KIND.PLAIN
116
+ },
117
+ {
118
+ name: "sdk_version",
119
+ value: raw.sdkVersion.join("."),
120
+ kind: KIND.PLAIN
121
+ },
122
+ clockField("now_ms", raw.nowMs),
123
+ clockField("ink_ms", raw.inkMs, "now_ms - 1,SDK 对自己的存活检查"),
124
+ {
125
+ name: "aid",
126
+ value: String(raw.aid),
127
+ kind: KIND.PLAIN
128
+ },
129
+ {
130
+ name: "page_id",
131
+ value: String(raw.pageId),
132
+ kind: KIND.PLAIN
133
+ },
134
+ {
135
+ name: "fortnights",
136
+ value: `${fortnights} (${instant(require_tokens.FORTNIGHT_EPOCH_MS + fortnights * FORTNIGHT_MS)})`,
137
+ kind: KIND.PLAIN,
138
+ detail: "自 a_bogus 纪元起的「两周」数"
139
+ },
140
+ {
141
+ name: "browser_info",
142
+ value: raw.browserInfo,
143
+ kind: KIND.ENVIRONMENT,
144
+ detail: "屏幕|可视区|…"
145
+ },
146
+ {
147
+ name: "tail",
148
+ value: raw.tail,
149
+ kind: KIND.PLAIN,
150
+ detail: "(now_ms + 3) & 0xFF"
151
+ },
152
+ {
153
+ name: "env_flags",
154
+ value: String(raw.envFlags),
155
+ kind: KIND.ENVIRONMENT
156
+ },
157
+ {
158
+ name: "detect_flags",
159
+ value: String(raw.detectFlags),
160
+ kind: KIND.ENVIRONMENT
161
+ },
162
+ {
163
+ name: "call_bucket",
164
+ value: String(raw.callBucket),
165
+ kind: KIND.ENVIRONMENT
166
+ },
167
+ {
168
+ name: "tripwire",
169
+ value: String(raw.tripwire),
170
+ kind: KIND.ENVIRONMENT
171
+ }
172
+ ];
173
+ const inputByChain = {
174
+ query: candidates.query,
175
+ body: candidates.body,
176
+ user_agent: candidates.userAgent
177
+ };
178
+ const emptyBody = require_tokens.chainBytes(require_tokens.DIGEST_CHAINS.body, require_tokens.digestWith("", require_tokens.SALT));
179
+ const checks = [];
180
+ for (const [name, chain] of Object.entries(require_tokens.DIGEST_CHAINS)) {
181
+ const carried = chain.slots.map((slot) => scalars[slot]);
182
+ let detail = `${name} 链的 3 个字节,落在 ${chain.slots.join(", ")}`;
183
+ if (name === "body" && carried.join(",") === emptyBody.join(",")) detail = `${detail};与空 body 一致,GET 就是这种情况`;
184
+ fields.push({
185
+ name: `${name}_digest`,
186
+ value: carried.join(" "),
187
+ kind: KIND.DIGEST,
188
+ detail
189
+ });
190
+ const candidate = inputByChain[name];
191
+ if (candidate === void 0) {
192
+ checks.push({
193
+ name,
194
+ status: CHECK.NOT_SUPPLIED,
195
+ bits: CHAIN_BITS
196
+ });
197
+ continue;
198
+ }
199
+ const digest = name === "user_agent" ? require_tokens.userAgentDigest(candidate, {
200
+ envFlags: raw.envFlags,
201
+ detectFlags: raw.detectFlags
202
+ }) : require_tokens.digestWith(candidate, require_tokens.SALT);
203
+ checks.push(check(name, require_tokens.chainBytes(chain, digest), carried, candidate));
204
+ }
205
+ return {
206
+ parameter: "a_bogus",
207
+ platform: "douyin",
208
+ algorithm: "a_bogus",
209
+ recovered: true,
210
+ fields,
211
+ checks,
212
+ notes: ["checksum_verified", "noise_not_recoverable"]
213
+ };
214
+ };
215
+ /**
216
+ * 反推一份签名用的是哪个盐值 —— **这个工具存在的核心理由**。
217
+ *
218
+ * 做法与当初发现 `cus` 过期时完全一样:签名里封着 query 链的三个字节,把候选盐值逐个
219
+ * 代进去重算摘要,能重现那三个字节的就是当前盐值。整件事不需要网络、不需要身份、
220
+ * 也不依赖抖音是否接受了你的请求——这正是它能发现「请求照样成功但常量已经过期」的原因。
221
+ *
222
+ * ## 只有 query 链参与判定
223
+ *
224
+ * body 链在 GET 上与空串一致,而 UA 链**根本不吃盐值**(它走 RC4 加单次 SM3),
225
+ * 两条对判定都没有贡献,所以这里的候选盐值只对 query 生效。证据量是 24 位,
226
+ * 实战足够——但要说明,而不是藏起来。
227
+ * @param value - 一份真实的 `a_bogus`
228
+ * @param query - 该签名所覆盖的那条 query(不含 `a_bogus` 自身)
229
+ * @param salts - 待测的盐值候选
230
+ * @returns 判定结果
231
+ */
232
+ const diagnoseSalt = (value, query, salts = [require_tokens.SALT, "cus"]) => {
233
+ if (require_tokens.structureError(value) !== null) return {
234
+ salt: null,
235
+ observed: [],
236
+ results: []
237
+ };
238
+ const chain = require_tokens.DIGEST_CHAINS.query;
239
+ const scalars = require_tokens.decode(value).fields;
240
+ const observed = chain.slots.map((slot) => scalars[slot]);
241
+ const results = salts.map((salt) => {
242
+ const predicted = require_tokens.chainBytes(chain, require_tokens.digestWith(query, salt));
243
+ return {
244
+ salt,
245
+ predicted,
246
+ matched: predicted.join(",") === observed.join(",")
247
+ };
248
+ });
249
+ return {
250
+ salt: results.find((row) => row.matched)?.salt ?? null,
251
+ observed,
252
+ results
253
+ };
254
+ };
255
+ /**
256
+ * 只按形状猜一个值是什么参数。用于调用方不知道自己在看什么的时候。
257
+ *
258
+ * 只做形状判断,不做任何密码学断言——猜错时返回 `null` 比返回一个错答案好。
259
+ * @param value - 待识别的值
260
+ * @returns 参数名或 `null`
261
+ */
262
+ const identify = (value) => {
263
+ if (!value) return null;
264
+ if (require_tokens.structureError(value) === null) return "a_bogus";
265
+ if (/^verify_[0-9a-z]+_/.test(value)) return "verifyFp";
266
+ if (value.length === 28 && /^[A-Za-z0-9_=+/-]+$/.test(value)) return "X-Bogus";
267
+ if (value.length >= 100 && /^[A-Za-z0-9+-]+=*$/.test(value)) return "msToken";
268
+ if (/^[0-9a-f]{32}$/i.test(value)) return "x-secsdk-web-signature";
269
+ return null;
270
+ };
271
+ /**
272
+ * 签名**一定**不覆盖的参数。
273
+ *
274
+ * `a_bogus` 是签名自身;`timestamp` 与 `x-secsdk-web-signature` 由 secsdk 在签名
275
+ * **之后**追加 —— 这一点在真实捕获与 amagi 自己的管线里一致。
276
+ */
277
+ const NEVER_SIGNED = [
278
+ "a_bogus",
279
+ "timestamp",
280
+ "x-secsdk-web-signature"
281
+ ];
282
+ /**
283
+ * 是否被签名覆盖**随管线而异**的参数。
284
+ *
285
+ * 不要猜。真实浏览器捕获里 `uifid` 在 `a_bogus` **之前**(因而被签名覆盖),
286
+ * 而 amagi 的管线是 `webid → a_bogus → secsdk`,`uifid` 由 secsdk 在之后补上
287
+ * (因而没被覆盖)。同一个参数名,两边结论相反 —— 所以这里把四种参数做成
288
+ * 2⁴ 种组合逐个试,命中的那一种就是答案。
289
+ */
290
+ const SIGNING_AMBIGUOUS = [
291
+ "uifid",
292
+ "msToken",
293
+ "verifyFp",
294
+ "fp"
295
+ ];
296
+ /**
297
+ * 反推一份签名**当时覆盖的那条 query**,以及用的是哪个盐值。
298
+ *
299
+ * 这是 {@link diagnoseSalt} 的加强版:那份要求调用方已经知道签名覆盖了哪些参数,
300
+ * 而线上抓来的 URL 里,`uifid` / `msToken` / `verifyFp` / `fp` 到底在签名前还是
301
+ * 签名后,取决于页面当时挂的是哪条管线 —— 同一份代码在浏览器里和在 amagi 里顺序
302
+ * 就是不一样的。与其让调用方去猜,这里把 2⁴ 种组合全试一遍,报出命中的那一种。
303
+ *
304
+ * 命中意味着**两件事同时成立**:这是一份结构良好的 a_bogus,且它的 query 链在给定
305
+ * 盐值下重现了这条 query。所以它是「验证」而不只是「解码」。
306
+ * @param signature - `a_bogus` 的值(未做 URL 编码)
307
+ * @param url - 已签名的完整 URL
308
+ * @param salts - 待搜的盐值候选
309
+ * @returns 命中结果;没有任何组合命中时返回 `null`
310
+ */
311
+ const recoverSignedQuery = (signature, url, salts = [require_tokens.SALT]) => {
312
+ if (require_tokens.structureError(signature) !== null) return null;
313
+ const chain = require_tokens.DIGEST_CHAINS.query;
314
+ const scalars = require_tokens.decode(signature).fields;
315
+ const observed = chain.slots.map((slot) => scalars[slot]).join(",");
316
+ let tried = 0;
317
+ for (let mask = 0; mask < 1 << SIGNING_AMBIGUOUS.length; mask++) {
318
+ const kept = SIGNING_AMBIGUOUS.filter((_, index) => (mask & 1 << index) !== 0);
319
+ const parsed = new URL(url);
320
+ for (const name of NEVER_SIGNED) parsed.searchParams.delete(name);
321
+ for (const name of SIGNING_AMBIGUOUS) if (!kept.includes(name)) parsed.searchParams.delete(name);
322
+ const query = parsed.searchParams.toString();
323
+ for (const salt of salts) {
324
+ tried++;
325
+ if (require_tokens.chainBytes(chain, require_tokens.digestWith(query, salt)).join(",") === observed) return {
326
+ query,
327
+ kept,
328
+ salt,
329
+ tried
330
+ };
331
+ }
332
+ }
333
+ return null;
334
+ };
335
+ /**
336
+ * 从一条已签名的 URL 里重建出签名覆盖的那条 query(不再搜索,按固定规则摘参数)。
337
+ *
338
+ * **只在已知管线顺序时使用。** 面对来源不明的 URL 请用 {@link recoverSignedQuery},
339
+ * 它会搜索并告诉你哪条重建成立。保留这个函数是为了让「摘掉签名参数」这件事在
340
+ * 需要确定行为的地方仍然可用。
341
+ * @param url - 已签名的完整 URL
342
+ * @returns 摘掉 `a_bogus` / `timestamp` / `x-secsdk-web-signature` 之后的 query
343
+ */
344
+ const signedQueryOf = (url) => {
345
+ const parsed = new URL(url);
346
+ for (const name of NEVER_SIGNED) parsed.searchParams.delete(name);
347
+ return parsed.searchParams.toString();
348
+ };
349
+ /**
350
+ * 拆开一条 URL 上所有认得出的签名参数。
351
+ *
352
+ * 会先尝试 {@link recoverSignedQuery} 找出签名真正覆盖的那条 query;找不到时退回
353
+ * {@link signedQueryOf} 的结果,并把这个不确定性体现在 `notes` 里。
354
+ * @param url - 已签名的完整 URL
355
+ * @param options - 可选的 User-Agent、显式 query 与盐值候选
356
+ * @returns 每个参数一条拆解结果
357
+ */
358
+ const decodeUrl = (url, options = {}) => {
359
+ const aBogus = new URL(url).searchParams.get("a_bogus");
360
+ if (aBogus === null) return [];
361
+ const signature = decodeURIComponent(aBogus);
362
+ const recovery = options.query ? null : recoverSignedQuery(signature, url, options.salts ?? ["dhzx"]);
363
+ const query = options.query ?? recovery?.query ?? signedQueryOf(url);
364
+ const decoded = decodeABogus(signature, {
365
+ query,
366
+ body: options.body,
367
+ userAgent: options.userAgent
368
+ });
369
+ if (recovery) {
370
+ decoded.notes.push(`query_recovered:kept=${recovery.kept.join("+") || "(none)"}`);
371
+ decoded.fields.push({
372
+ name: "signed_query",
373
+ value: recovery.query,
374
+ kind: KIND.PLAIN,
375
+ detail: `从 ${recovery.tried} 种组合中命中:保留了 ${recovery.kept.join("、") || "(不保留任何可选参数)"}`
376
+ });
377
+ } else if (!options.query) decoded.notes.push("query_reconstructed_by_rule:no_combination_matched");
378
+ return [decoded];
379
+ };
380
+ //#endregion
381
+ exports.ABogus = require_tokens.ABogus;
382
+ exports.AID = require_tokens.AID;
383
+ exports.ALPHABETS = require_tokens.ALPHABETS;
384
+ exports.CALL_BUCKET = require_tokens.CALL_BUCKET;
385
+ exports.CHECK = CHECK;
386
+ exports.DEFAULT_BROWSER_INFO = require_tokens.DEFAULT_BROWSER_INFO;
387
+ exports.DETECT_FLAGS = require_tokens.DETECT_FLAGS;
388
+ exports.DIGEST_CHAINS = require_tokens.DIGEST_CHAINS;
389
+ exports.DOUYIN_MS_TOKEN_LENGTH = require_tokens.DOUYIN_MS_TOKEN_LENGTH;
390
+ exports.DOUYIN_MS_TOKEN_SIZES = require_tokens.DOUYIN_MS_TOKEN_SIZES;
391
+ exports.DOUYIN_TTWID = require_tokens.DOUYIN_TTWID;
392
+ exports.DOUYIN_TTWID_PAYLOAD = require_tokens.DOUYIN_TTWID_PAYLOAD;
393
+ exports.ENV_FLAGS = require_tokens.ENV_FLAGS;
394
+ exports.FIELD_ORDER = require_tokens.FIELD_ORDER;
395
+ exports.FORTNIGHT_EPOCH_MS = require_tokens.FORTNIGHT_EPOCH_MS;
396
+ exports.HEADER_MAGIC = require_tokens.HEADER_MAGIC;
397
+ exports.KIND = KIND;
398
+ exports.MS_TOKEN_ALPHABET = require_tokens.MS_TOKEN_ALPHABET;
399
+ exports.PAGE_ID = require_tokens.PAGE_ID;
400
+ exports.PAYLOAD_KEY = require_tokens.PAYLOAD_KEY;
401
+ exports.REASON = REASON;
402
+ exports.SALT = require_tokens.SALT;
403
+ exports.SDK_VERSION = require_tokens.SDK_VERSION;
404
+ exports.SM3_IV = require_tokens.SM3_IV;
405
+ exports.TRIPWIRE_LOCKED = require_tokens.TRIPWIRE_LOCKED;
406
+ exports.TTWID_REGISTER_URL = require_tokens.TTWID_REGISTER_URL;
407
+ exports.VERIFY_FP_ALPHABET = require_tokens.VERIFY_FP_ALPHABET;
408
+ exports.VERIFY_FP_LENGTH = require_tokens.VERIFY_FP_LENGTH;
409
+ exports.browserInfoFromScreen = require_tokens.browserInfoFromScreen;
410
+ exports.buildBrowserInfo = require_tokens.buildBrowserInfo;
411
+ exports.canary = require_tokens.canary;
412
+ exports.chainBytes = require_tokens.chainBytes;
413
+ exports.decode = require_tokens.decode;
414
+ exports.decodeABogus = decodeABogus;
415
+ exports.decodeBase64 = require_tokens.decodeBase64;
416
+ exports.decodeUrl = decodeUrl;
417
+ exports.diagnoseSalt = diagnoseSalt;
418
+ exports.digestOf = require_tokens.digestOf;
419
+ exports.digestWith = require_tokens.digestWith;
420
+ exports.encodeBase64 = require_tokens.encodeBase64;
421
+ exports.genFalseMsToken = require_tokens.genFalseMsToken;
422
+ exports.genSVWebId = require_tokens.genSVWebId;
423
+ exports.genVerifyFp = require_tokens.genVerifyFp;
424
+ exports.identify = identify;
425
+ exports.isDecodeProblem = require_tokens.isDecodeProblem;
426
+ exports.jsBytes = require_tokens.jsBytes;
427
+ exports.msTokenPayload = require_tokens.msTokenPayload;
428
+ exports.rc4 = require_tokens.rc4;
429
+ exports.recoverSignedQuery = recoverSignedQuery;
430
+ exports.signedQueryOf = signedQueryOf;
431
+ exports.sm3Hash = require_tokens.sm3Hash;
432
+ exports.sm3Hexdigest = require_tokens.sm3Hexdigest;
433
+ exports.sm3ToArray = require_tokens.sm3ToArray;
434
+ exports.structureError = require_tokens.structureError;
435
+ exports.toBase36 = require_tokens.toBase36;
436
+ exports.unmaskPair = require_tokens.unmaskPair;
437
+ exports.userAgentDigest = require_tokens.userAgentDigest;