karin-plugin-kkk 2.45.1-beta.3 → 2.45.1-beta.4
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/CHANGELOG.md +7 -0
- package/lib/build-metadata.json +5 -5
- package/lib/core_chunk/RiskVerify.js +3 -0
- package/lib/core_chunk/amagiClient.js +1 -1
- package/lib/core_chunk/db.js +1 -1
- package/lib/core_chunk/main.js +206 -13
- package/lib/root.js +1 -1
- package/lib/template-registry.js +1 -1
- package/lib/web.config.js +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
## [2.45.1-beta.4](https://github.com/ikenxuan/karin-plugin-kkk/compare/v2.45.1-beta.3...v2.45.1-beta.4) (2026-10-17)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### 🧰 其他更新
|
|
9
|
+
|
|
10
|
+
* 添加风控验证处理逻辑,支持失败重放与动态导入 ([ba32b48](https://github.com/ikenxuan/karin-plugin-kkk/commit/ba32b48aa3f79225439522756cc9e702543420d6))
|
|
11
|
+
|
|
5
12
|
## [2.45.1-beta.3](https://github.com/ikenxuan/karin-plugin-kkk/compare/v2.45.1-beta.2...v2.45.1-beta.3) (2026-10-16)
|
|
6
13
|
|
|
7
14
|
|
package/lib/build-metadata.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "2.45.1-beta.
|
|
3
|
-
"buildTime": "2026-10-
|
|
4
|
-
"buildTimestamp":
|
|
2
|
+
"version": "2.45.1-beta.4",
|
|
3
|
+
"buildTime": "2026-10-16T18:08:24.609Z",
|
|
4
|
+
"buildTimestamp": 1792174104609,
|
|
5
5
|
"name": "karin-plugin-kkk",
|
|
6
6
|
"description": "Karin 的「抖音」「B 站」视频解析/动态推送插件",
|
|
7
7
|
"homepage": "https://github.com/ikenxuan/karin-plugin-kkk",
|
|
8
|
-
"commitHash": "
|
|
9
|
-
"shortCommitHash": "
|
|
8
|
+
"commitHash": "0a9385f0228e74edb394719ef42ccf11b8b4d5fa",
|
|
9
|
+
"shortCommitHash": "0a9385f0"
|
|
10
10
|
}
|
package/lib/core_chunk/db.js
CHANGED
package/lib/core_chunk/main.js
CHANGED
|
@@ -19,6 +19,7 @@ import { Transform } from "node:stream";
|
|
|
19
19
|
import { pipeline } from "node:stream/promises";
|
|
20
20
|
import _ from "node-karin/lodash";
|
|
21
21
|
import util from "node:util";
|
|
22
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
22
23
|
import os from "node:os";
|
|
23
24
|
//#region src/index.ts
|
|
24
25
|
globalThis.__kkkLoadStart ??= process.hrtime.bigint();
|
|
@@ -461,20 +462,58 @@ var isFailureEnvelope = (value) => {
|
|
|
461
462
|
/** 判断是不是 thenable,用来只包装异步方法 */
|
|
462
463
|
var isThenable = (value) => !!value && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
|
|
463
464
|
/**
|
|
465
|
+
* 把一次 fetcher 调用的结果收口:成功原样返回,失败信封先给 RiskVerify 一次「发码并轮询」的机会。
|
|
466
|
+
*
|
|
467
|
+
* `handleRiskChallenge` 对不带 `challenge.url` 的失败会立刻返回 false(是不是风控由它判定),
|
|
468
|
+
* 所以这里不用先判。过码成功后按它的契约**再重放一次**原调用,这次的返回值就是结果;
|
|
469
|
+
* 没放行(超时 / 没有消息上下文 / 达并发上限)则把原错误抛出去。
|
|
470
|
+
*
|
|
471
|
+
* RiskVerify 用**动态 import** 取,不能提成顶层静态 import:它的链上有
|
|
472
|
+
* `Render → @/module(barrel)→ ./utils → Base → amagiClient`,静态边会让「先加载 amagiClient」
|
|
473
|
+
* 的模块求值顺序撞上 `Base extends AmagiBase` 的 TDZ(dev 下必崩),本文件与 Base 之间就成环了。
|
|
474
|
+
* 挪到运行期后,这条边只在失败路径、模块图就绪之后才走。
|
|
475
|
+
* @param returned - 原调用的返回值(已排除同步方法)
|
|
476
|
+
* @param platform - 平台名,RiskVerify 用它做去重键与提示文案
|
|
477
|
+
* @param call - 原调用的重放:同一方法、同一参数、同一份 CK
|
|
478
|
+
*/
|
|
479
|
+
var settleFailure = async (returned, platform, call) => {
|
|
480
|
+
const result = await returned;
|
|
481
|
+
if (!isFailureEnvelope(result)) return result;
|
|
482
|
+
const failure = new AmagiError(result);
|
|
483
|
+
const { handleRiskChallenge } = await import("./RiskVerify.js");
|
|
484
|
+
if (!await handleRiskChallenge(failure, platform, () => probeReplay(call))) throw failure;
|
|
485
|
+
const replayed = await call();
|
|
486
|
+
if (isFailureEnvelope(replayed)) throw new AmagiError(replayed);
|
|
487
|
+
return replayed;
|
|
488
|
+
};
|
|
489
|
+
/**
|
|
490
|
+
* 给 RiskVerify 轮询用的重放探针。
|
|
491
|
+
*
|
|
492
|
+
* 判定权在它那边:true = 已放行;仍被同一类风控拦住返回 false(继续等);
|
|
493
|
+
* 冒出其它失败就抛,RiskVerify 据此停止等待、把错误交回原调用方处理。
|
|
494
|
+
*/
|
|
495
|
+
var probeReplay = async (call) => {
|
|
496
|
+
const result = await call();
|
|
497
|
+
if (!isFailureEnvelope(result)) return true;
|
|
498
|
+
if (result.error.challenge?.url) return false;
|
|
499
|
+
throw new AmagiError(result);
|
|
500
|
+
};
|
|
501
|
+
/**
|
|
464
502
|
* 递归代理一个 fetcher 对象,把失败信封转成 `throw AmagiError`。返回类型与入参同形 ——
|
|
465
503
|
* 「只保留成功分支」是**类型层**由 {@link ThrowingClient} 声明的,这里只管运行时行为。
|
|
504
|
+
*
|
|
505
|
+
* 带风控挑战的失败不直接抛:先交给 `RiskVerify` 走完整条验证流程(发二维码给主人 → 轮询重放),
|
|
506
|
+
* 过码成功就重放拿正常结果,没成功再把原错误抛出去 —— 见 {@link settleFailure}。
|
|
466
507
|
*/
|
|
467
|
-
var throwOnFailure = (target) => new Proxy(target, { get(obj, prop) {
|
|
508
|
+
var throwOnFailure = (target, platform) => new Proxy(target, { get(obj, prop) {
|
|
468
509
|
const value = obj[prop];
|
|
469
510
|
if (typeof value === "function") return (...args) => {
|
|
470
|
-
const
|
|
511
|
+
const call = () => value.apply(obj, args);
|
|
512
|
+
const returned = call();
|
|
471
513
|
if (!isThenable(returned)) return returned;
|
|
472
|
-
return returned
|
|
473
|
-
if (isFailureEnvelope(result)) throw new AmagiError(result);
|
|
474
|
-
return result;
|
|
475
|
-
});
|
|
514
|
+
return settleFailure(returned, platform, call);
|
|
476
515
|
};
|
|
477
|
-
if (value && typeof value === "object" && !Array.isArray(value)) return throwOnFailure(value);
|
|
516
|
+
if (value && typeof value === "object" && !Array.isArray(value)) return throwOnFailure(value, platform);
|
|
478
517
|
return value;
|
|
479
518
|
} });
|
|
480
519
|
/** 解析库基类 */
|
|
@@ -534,19 +573,19 @@ var AmagiBase = class {
|
|
|
534
573
|
...client,
|
|
535
574
|
bilibili: {
|
|
536
575
|
...client.bilibili,
|
|
537
|
-
fetcher: throwOnFailure(client.bilibili.fetcher)
|
|
576
|
+
fetcher: throwOnFailure(client.bilibili.fetcher, "bilibili")
|
|
538
577
|
},
|
|
539
578
|
douyin: {
|
|
540
579
|
...client.douyin,
|
|
541
|
-
fetcher: throwOnFailure(client.douyin.fetcher)
|
|
580
|
+
fetcher: throwOnFailure(client.douyin.fetcher, "douyin")
|
|
542
581
|
},
|
|
543
582
|
kuaishou: {
|
|
544
583
|
...client.kuaishou,
|
|
545
|
-
fetcher: throwOnFailure(client.kuaishou.fetcher)
|
|
584
|
+
fetcher: throwOnFailure(client.kuaishou.fetcher, "kuaishou")
|
|
546
585
|
},
|
|
547
586
|
xiaohongshu: {
|
|
548
587
|
...client.xiaohongshu,
|
|
549
|
-
fetcher: throwOnFailure(client.xiaohongshu.fetcher)
|
|
588
|
+
fetcher: throwOnFailure(client.xiaohongshu.fetcher, "xiaohongshu")
|
|
550
589
|
}
|
|
551
590
|
});
|
|
552
591
|
};
|
|
@@ -29947,6 +29986,159 @@ var buildErrorPrefix = async (ctx, isPush, botId) => {
|
|
|
29947
29986
|
return `群:${(await karin.getBot(botId)?.getGroupInfo(groupId))?.groupName || "未知"}(${groupId})\n${options.businessName} 任务执行出错\n请尽快解决以消除警告`;
|
|
29948
29987
|
};
|
|
29949
29988
|
//#endregion
|
|
29989
|
+
//#region src/module/utils/RiskVerify/index.ts
|
|
29990
|
+
/**
|
|
29991
|
+
* 风控验证(人机验证)自动监听。
|
|
29992
|
+
*
|
|
29993
|
+
* ## 立场
|
|
29994
|
+
*
|
|
29995
|
+
* 过码的人是**主人**(CK 所有者),不是触发解析的群友 —— 风控绑的是机器人这份 CK 的会话,
|
|
29996
|
+
* 只有账号主人能在自己手机上过码。群友全程不参与、也不用回复任何消息。
|
|
29997
|
+
*
|
|
29998
|
+
* ## 判定靠轮询,不靠用户回执
|
|
29999
|
+
*
|
|
30000
|
+
* 平台没有「过码完成」的推送通道,验证是服务端按 `fp`(抖音)/`did`(快手)/`verifyUuid`
|
|
30001
|
+
* (小红书)解除的。所以过码完成的唯一可观测信号是:**用同一份 CK、同一组参数重放原请求,
|
|
30002
|
+
* 它不再被拦** —— 这就是 amagi 文档里那句「过完由调用方用同一份 CK 重发」的自然实现。
|
|
30003
|
+
*
|
|
30004
|
+
* ## 撤回预算(硬约束)
|
|
30005
|
+
*
|
|
30006
|
+
* 消息撤回窗口 120s,本模块的决策线是 {@link VERIFY_WINDOW_MS} = 110s,留 10s 给网络与
|
|
30007
|
+
* 程序时延。**超过决策线一律不再尝试撤回**(必然失败),改为补一条状态说明。
|
|
30008
|
+
*
|
|
30009
|
+
* ## AsyncLocalStorage 的纪律(本仓首例,照做)
|
|
30010
|
+
*
|
|
30011
|
+
* - 只用 `run()`,**绝不用 `enterWith()`** —— 后者会污染不相关的后续任务。
|
|
30012
|
+
* - store 里只放渲染/投递要用的几个字段,**不放整个 `e`**:这样即使一条流程挂满 110s,
|
|
30013
|
+
* 被钉住的也只有几百字节(handler 的闭包本来就持有 `e`,ALS 不额外延长它的生命周期)。
|
|
30014
|
+
* - 本模块是唯一的模块级状态({@link pending}),条目在 `finally` 里清理,且**有硬上限**。
|
|
30015
|
+
*/
|
|
30016
|
+
/** 一条验证流程的时间窗:撤回窗口 120s − 10s 余量 */
|
|
30017
|
+
var VERIFY_WINDOW_MS = 11e4;
|
|
30018
|
+
/** 重放间隔 */
|
|
30019
|
+
var POLL_INTERVAL_MS = 1e4;
|
|
30020
|
+
/** 超时后的冷却窗:这段时间内再撞风控不再发图(防刷屏) */
|
|
30021
|
+
var COOLDOWN_MS = 6e4;
|
|
30022
|
+
/** pending 表硬上限:极端高频时直接放弃自动验证,抛原错误 */
|
|
30023
|
+
var MAX_PENDING = 8;
|
|
30024
|
+
var storage = new AsyncLocalStorage();
|
|
30025
|
+
/** 在命令入口包一层,把渲染上下文带进这条异步链 */
|
|
30026
|
+
var runWithVerifyContext = (event, fn) => storage.run({ event }, fn);
|
|
30027
|
+
/** 当前是否处于某个消息处理链里(定时推送等场景为空) */
|
|
30028
|
+
var currentContext = () => storage.getStore();
|
|
30029
|
+
var pending = /* @__PURE__ */ new Map();
|
|
30030
|
+
var cooldownUntil = 0;
|
|
30031
|
+
/** challenge 的稳定键:平台 + 票据/地址,用于并发去重 */
|
|
30032
|
+
var keyOf = (platform, url, session) => `${platform}:${session ?? ""}:${url}`;
|
|
30033
|
+
/**
|
|
30034
|
+
* 把验证页渲染成图片:有渲染上下文走模板(视觉优先),没有就退回纯二维码。
|
|
30035
|
+
* @param platform - 平台名(用于模板标签)
|
|
30036
|
+
* @param url - 平台官方验证页地址
|
|
30037
|
+
* @param context - ALS 里的渲染上下文,可能为空(定时推送)
|
|
30038
|
+
* @returns 可直接发送的图片消息段
|
|
30039
|
+
*/
|
|
30040
|
+
var renderVerifyImage = async (platform, url, context) => {
|
|
30041
|
+
if (context) try {
|
|
30042
|
+
const images = await Render(context.event, "other/verifyQr", {
|
|
30043
|
+
share_url: url,
|
|
30044
|
+
platform,
|
|
30045
|
+
expiresHint: "请在 2 分钟内完成"
|
|
30046
|
+
});
|
|
30047
|
+
if (images.length > 0) return images;
|
|
30048
|
+
} catch (error) {
|
|
30049
|
+
logger$1.warn(`[风控验证] 模板渲染失败,退回纯二维码: ${error}`);
|
|
30050
|
+
}
|
|
30051
|
+
return [segment.image(generateQRCode(url, false))];
|
|
30052
|
+
};
|
|
30053
|
+
/**
|
|
30054
|
+
* 私聊主人:验证二维码 + 说明。主人不在配置里时不发,直接返回 false。
|
|
30055
|
+
* @param platform - 平台名
|
|
30056
|
+
* @param url - 验证页地址
|
|
30057
|
+
* @returns 是否成功送达
|
|
30058
|
+
*/
|
|
30059
|
+
var notifyMaster = async (platform, url) => {
|
|
30060
|
+
const context = currentContext();
|
|
30061
|
+
if (!context) {
|
|
30062
|
+
logger$1.warn(`[风控验证] ${platform} 需要人机验证,但当前调用没有消息上下文,无法通知主人`);
|
|
30063
|
+
return false;
|
|
30064
|
+
}
|
|
30065
|
+
const images = await renderVerifyImage(platform, url, context);
|
|
30066
|
+
const target = await resolveSingleMasterTarget(context.event, false);
|
|
30067
|
+
if (!target) {
|
|
30068
|
+
logger$1.warn(`[风控验证] ${platform} 需要人机验证,但没有可用的主人私聊目标`);
|
|
30069
|
+
return false;
|
|
30070
|
+
}
|
|
30071
|
+
await karin$1.sendMaster(target.botId, target.master, [segment.text(`【${platform}】需要一次人机验证\n请用手机扫码,在打开的官方页面里完成验证。
|
|
30072
|
+
完成后不需要回复:我会自动检测到并继续刚才的解析。
|
|
30073
|
+
超过 ${VERIFY_WINDOW_MS / 1e3} 秒未完成会自动放弃,可稍后重新触发。`), ...images]);
|
|
30074
|
+
return true;
|
|
30075
|
+
};
|
|
30076
|
+
/**
|
|
30077
|
+
* 跑一条验证流程:发图 → 每 {@link POLL_INTERVAL_MS} 重放一次 → 成功即收口。
|
|
30078
|
+
* @param key - 去重键
|
|
30079
|
+
* @param platform - 平台名
|
|
30080
|
+
* @param url - 验证页地址
|
|
30081
|
+
* @param replay - 用同一份 CK 重放原调用,返回 true 表示已放行
|
|
30082
|
+
* @returns 是否在决策线内过码成功
|
|
30083
|
+
*/
|
|
30084
|
+
var runVerify = async (key, platform, url, replay) => {
|
|
30085
|
+
const deadline = Date.now() + VERIFY_WINDOW_MS;
|
|
30086
|
+
const notified = await notifyMaster(platform, url);
|
|
30087
|
+
while (Date.now() < deadline) {
|
|
30088
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
30089
|
+
try {
|
|
30090
|
+
if (await replay()) {
|
|
30091
|
+
logger$1.mark(`[风控验证] ${platform} 已放行(${key})`);
|
|
30092
|
+
return true;
|
|
30093
|
+
}
|
|
30094
|
+
} catch (error) {
|
|
30095
|
+
logger$1.debug(`[风控验证] ${platform} 重放返回其它错误,停止等待: ${error}`);
|
|
30096
|
+
return false;
|
|
30097
|
+
}
|
|
30098
|
+
}
|
|
30099
|
+
cooldownUntil = Date.now() + COOLDOWN_MS;
|
|
30100
|
+
logger$1.warn(`[风控验证] ${platform} 在 ${VERIFY_WINDOW_MS / 1e3}s 内未完成${notified ? "(已通知主人)" : ""}`);
|
|
30101
|
+
return false;
|
|
30102
|
+
};
|
|
30103
|
+
/**
|
|
30104
|
+
* 判断这个错误是不是「可交给主人过码」的风控,并在需要时跑完整条验证流程。
|
|
30105
|
+
*
|
|
30106
|
+
* 调用方(amagiClient 的失败代理)拿到 `false` 就照原样抛出原错误;
|
|
30107
|
+
* 拿到 `true` 说明已经放行,**重放一次原调用即可拿到正常结果**。
|
|
30108
|
+
* @param error - 代理捕获到的 AmagiError
|
|
30109
|
+
* @param platform - 平台名
|
|
30110
|
+
* @param replay - 重放原调用(同一方法、同一参数、同一份 CK)
|
|
30111
|
+
* @returns 是否已放行
|
|
30112
|
+
*/
|
|
30113
|
+
var handleRiskChallenge = async (error, platform, replay) => {
|
|
30114
|
+
const url = error.rawError?.challenge?.url;
|
|
30115
|
+
if (!url) return false;
|
|
30116
|
+
if (!currentContext()) return false;
|
|
30117
|
+
if (pending.size >= MAX_PENDING) {
|
|
30118
|
+
logger$1.warn(`[风控验证] 进行中的验证已达上限 ${MAX_PENDING},本次不做自动验证`);
|
|
30119
|
+
return false;
|
|
30120
|
+
}
|
|
30121
|
+
if (Date.now() < cooldownUntil) {
|
|
30122
|
+
logger$1.debug(`[风控验证] ${platform} 处于冷却窗内,跳过`);
|
|
30123
|
+
return false;
|
|
30124
|
+
}
|
|
30125
|
+
const key = keyOf(platform, url, error.rawError?.challenge?.session);
|
|
30126
|
+
const existing = pending.get(key);
|
|
30127
|
+
if (existing) {
|
|
30128
|
+
existing.joined += 1;
|
|
30129
|
+
return existing.settled;
|
|
30130
|
+
}
|
|
30131
|
+
const entry = {
|
|
30132
|
+
joined: 1,
|
|
30133
|
+
settled: Promise.resolve(false)
|
|
30134
|
+
};
|
|
30135
|
+
entry.settled = runVerify(key, platform, url, replay).finally(() => {
|
|
30136
|
+
pending.delete(key);
|
|
30137
|
+
});
|
|
30138
|
+
pending.set(key, entry);
|
|
30139
|
+
return entry.settled;
|
|
30140
|
+
};
|
|
30141
|
+
//#endregion
|
|
29950
30142
|
//#region src/module/utils/ErrorHandler/handler.ts
|
|
29951
30143
|
var handleBusinessError = async (error, options, logs, event) => {
|
|
29952
30144
|
try {
|
|
@@ -29991,7 +30183,8 @@ var wrapWithErrorHandler = (fn, options) => {
|
|
|
29991
30183
|
emojiManager.add("PROCESSING").catch(() => {});
|
|
29992
30184
|
}, 1500);
|
|
29993
30185
|
}
|
|
29994
|
-
const
|
|
30186
|
+
const runHandler = () => fn(normalizedEvent, normalizedNext);
|
|
30187
|
+
const ctx = logger$1.runContext(async () => rawEvent ? runWithVerifyContext(rawEvent, runHandler) : runHandler());
|
|
29995
30188
|
try {
|
|
29996
30189
|
const result = await ctx.run();
|
|
29997
30190
|
if (emojiManager) successTimer = setTimeout(() => {
|
|
@@ -42907,4 +43100,4 @@ mkdirSync(`${karinPathBase}/${Root.pluginName}/data`);
|
|
|
42907
43100
|
mkdirSync(Common.tempDri.images);
|
|
42908
43101
|
mkdirSync(Common.tempDri.video);
|
|
42909
43102
|
//#endregion
|
|
42910
|
-
export {
|
|
43103
|
+
export { douyinFetcher as $, task as A, getBilibiliDB as B, help as C, dylogin as D, biRefreshLogin as E, templates as F, StatisticsDBBase as G, getStatisticsDB as H, webConfig as I, BilibiliDBBase as J, resolveMetricBucket as K, bilibiliDBInstance as L, xhsLogin as M, handleRiskChallenge as N, ksLogin as O, runWithVerifyContext as P, bilibiliFetcher as Q, cleanOldDynamicCache as R, changelog as S, biLogin as T, initAllDatabases as U, getDouyinDB as V, METRIC_BUCKETS as W, AmagiError as X, AmagiBase as Y, SOFT_ERROR_CODES as Z, douyinPushList as _, bilibiliAPP as a, softFetch as at, setbiliPush as b, prefix as c, globalStatistics as d, getAmagiClient as et, groupStatistics as f, douyinPush as g, bilibiliPushList as h, update as i, reloadAmagiConfig as it, testWrapWithErrorHandler as j, removeOldFiles as k, xiaohongshuAPP as l, bilibiliPush as m, kkkUpdateCommand as n, kuaishouFetcher as nt, douyinAPP as o, xiaohongshuFetcher as ot, qrLogin as p, DouyinDBBase as q, kkkUpdateTest as r, registerAmagiReloadListener as rt, kuaishouAPP as s, Root as st, kkkUpdate as t, isSoftFailure as tt, testPush as u, forcePush as v, version as w, setdyPush as x, globalIgnore as y, douyinDBInstance as z };
|
package/lib/root.js
CHANGED
package/lib/template-registry.js
CHANGED
package/lib/web.config.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "karin-plugin-kkk",
|
|
3
|
-
"version": "2.45.1-beta.
|
|
3
|
+
"version": "2.45.1-beta.4",
|
|
4
4
|
"description": "Karin 的「抖音」「B 站」视频解析/动态推送插件",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"karin",
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"@lobehub/webfont-harmony-sans-sc": "^1.0.0",
|
|
84
84
|
"emoji-datasource-apple": "16.0.0"
|
|
85
85
|
},
|
|
86
|
-
"timestamp": "2026-10-
|
|
86
|
+
"timestamp": "2026-10-16T18:07:33.612Z",
|
|
87
87
|
"engines": {
|
|
88
88
|
"node": ">=18.0.0"
|
|
89
89
|
}
|