dsh-approval-consult 1.1.1 → 1.1.3
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/README.en.md +2 -2
- package/README.md +3 -3
- package/lib/impl.js +161 -73
- package/package.json +2 -2
package/README.en.md
CHANGED
|
@@ -53,7 +53,7 @@ notes or reject blindly. This plugin adds a consultation panel on top of the app
|
|
|
53
53
|
|
|
54
54
|
Requires DSH Web `0.2.1-rc.2` (tested) and Node `>=20`.
|
|
55
55
|
|
|
56
|
-
Current version **v1.1.
|
|
56
|
+
Current version **v1.1.3**, published to npm ([`dsh-approval-consult`](https://www.npmjs.com/package/dsh-approval-consult)) and as a [GitHub release](https://github.com/0tests/dsh-approval-consult/releases/tag/v1.1.3).
|
|
57
57
|
|
|
58
58
|
```bash
|
|
59
59
|
npm install dsh-approval-consult
|
|
@@ -61,7 +61,7 @@ npm install dsh-approval-consult
|
|
|
61
61
|
|
|
62
62
|
Or install the release tarball:
|
|
63
63
|
|
|
64
|
-
1. Download `dsh-approval-consult-1.1.
|
|
64
|
+
1. Download `dsh-approval-consult-1.1.3.tgz` from
|
|
65
65
|
[Releases](https://github.com/0tests/dsh-approval-consult/releases/latest).
|
|
66
66
|
2. Open DSH Web → **Plugins** → **Add plugin** → **Install third-party plugin** and point it at the
|
|
67
67
|
tarball's absolute path (or the download URL directly).
|
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ Auto 审查(`@deepseek-ai/dsh-experimental-auto-review`)会在工具执行
|
|
|
37
37
|
|
|
38
38
|
## 安装
|
|
39
39
|
|
|
40
|
-
> 当前版本 **v1.1.
|
|
40
|
+
> 当前版本 **v1.1.3**,已发布到 npm([`dsh-approval-consult`](https://www.npmjs.com/package/dsh-approval-consult))和 [GitHub Release](https://github.com/0tests/dsh-approval-consult/releases/tag/v1.1.3)。
|
|
41
41
|
> `npm install dsh-approval-consult`,或让 Agent 用 `plugin_manager` 安装这个包名;下面也保留了 tarball / 本地目录两种方式。
|
|
42
42
|
|
|
43
43
|
### 方式 A:npm
|
|
@@ -49,10 +49,10 @@ Auto 审查(`@deepseek-ai/dsh-experimental-auto-review`)会在工具执行
|
|
|
49
49
|
### 方式 B:从 Release 安装(tarball)
|
|
50
50
|
|
|
51
51
|
1. 在 [Releases](https://github.com/0tests/dsh-approval-consult/releases/latest) 下载
|
|
52
|
-
`dsh-approval-consult-1.1.
|
|
52
|
+
`dsh-approval-consult-1.1.3.tgz`;
|
|
53
53
|
2. 打开 DSH Web → 左侧「插件」→「添加插件」→「安装第三方插件」,填这个 tarball 的绝对路径,
|
|
54
54
|
或直接填下载链接
|
|
55
|
-
`https://github.com/0tests/dsh-approval-consult/releases/download/v1.1.
|
|
55
|
+
`https://github.com/0tests/dsh-approval-consult/releases/download/v1.1.3/dsh-approval-consult-1.1.3.tgz`;
|
|
56
56
|
3. 安装完成后点「立即启用」,重启一次 `dsh web` 更稳。
|
|
57
57
|
|
|
58
58
|
### 方式 C:本地目录(源码 / 开发)
|
package/lib/impl.js
CHANGED
|
@@ -492,6 +492,8 @@ export function apply(ctx, config, runtime) {
|
|
|
492
492
|
jobs: new Map(),
|
|
493
493
|
/** uploadId -> 附件引用登记(图片只存引用,字节在 DSH 附件服务里) */
|
|
494
494
|
attachments: new Map(),
|
|
495
|
+
/** consultKey -> 进行中的上传预占数(先占名额再 saveImage,失败释放) */
|
|
496
|
+
attachmentPending: new Map(),
|
|
495
497
|
/** 侧调用 options 集合(对象身份),llm/stream 捕获器直接跳过 */
|
|
496
498
|
sideOptions: new WeakSet(),
|
|
497
499
|
/** 已被本插件包装/记录过的 options(避免根与 agent 两处监听重复处理) */
|
|
@@ -540,6 +542,7 @@ export function apply(ctx, config, runtime) {
|
|
|
540
542
|
state = previousState
|
|
541
543
|
// 瞬态字段一律重建;旧 jobs 已被上一代 dispose 中止。
|
|
542
544
|
state.jobs = new Map()
|
|
545
|
+
state.attachmentPending = new Map()
|
|
543
546
|
state.sideOptions = new WeakSet()
|
|
544
547
|
state.wrappedOptions = new WeakSet()
|
|
545
548
|
state.agentDisposers = new Map()
|
|
@@ -947,8 +950,9 @@ export function apply(ctx, config, runtime) {
|
|
|
947
950
|
}
|
|
948
951
|
|
|
949
952
|
/**
|
|
950
|
-
*
|
|
951
|
-
*
|
|
953
|
+
* 把这次审批和一条审查记录配对:只接受**可验证的关联**——参数完全一致,或审批理由里
|
|
954
|
+
* 原样包含审查理由。工具名相同不再构成匹配依据,也不跨 session 猜测(安全审查 2026-11-03)。
|
|
955
|
+
* 无法准确关联时返回 null,让面板显示「缺少审查材料」或对当前调用重新评估。
|
|
952
956
|
*/
|
|
953
957
|
function matchReview(sessionId, toolName, exec, reason) {
|
|
954
958
|
const list = state.reviews.get(sessionId)
|
|
@@ -962,45 +966,28 @@ export function apply(ctx, config, runtime) {
|
|
|
962
966
|
const action = review.action
|
|
963
967
|
if (action === null) continue
|
|
964
968
|
if (action.name !== toolName) continue
|
|
969
|
+
const reviewArgs = canonicalJson(normalizeArguments(action.arguments))
|
|
970
|
+
const argsKnown = execArgs !== undefined && reviewArgs.length > 0
|
|
971
|
+
const sameArgs = argsKnown && reviewArgs === execArgs
|
|
972
|
+
const reasonHit =
|
|
973
|
+
typeof reason === 'string' &&
|
|
974
|
+
typeof review.decision?.reason === 'string' &&
|
|
975
|
+
review.decision.reason.length >= 12 &&
|
|
976
|
+
reason.includes(review.decision.reason)
|
|
977
|
+
// 参数已知且不一致:无论理由文字是否命中都不复用——理由文本不能替代调用身份。
|
|
978
|
+
if (argsKnown && !sameArgs) continue
|
|
979
|
+
// 参数未知时,才退化为「理由原样命中」这一弱证据;两者都无法核对则不复用。
|
|
980
|
+
if (!sameArgs && !reasonHit) continue
|
|
965
981
|
let score = 1
|
|
966
|
-
if (
|
|
967
|
-
|
|
968
|
-
}
|
|
969
|
-
if (execArgs !== undefined) {
|
|
970
|
-
const reviewArgs = canonicalJson(normalizeArguments(action.arguments))
|
|
971
|
-
if (reviewArgs.length > 0 && reviewArgs === execArgs) score += 3
|
|
972
|
-
}
|
|
982
|
+
if (reasonHit) score += 4
|
|
983
|
+
if (sameArgs) score += 3
|
|
973
984
|
if (review.decision !== null) score += 1
|
|
974
985
|
if (score > bestScore || (score === bestScore && best !== null && review.at > best.at)) {
|
|
975
986
|
best = review
|
|
976
987
|
bestScore = score
|
|
977
988
|
}
|
|
978
989
|
}
|
|
979
|
-
|
|
980
|
-
const sameTool = candidates.filter((review) => review.action?.name === toolName)
|
|
981
|
-
if (sameTool.length === 1) return sameTool[0]
|
|
982
|
-
// 跨 session 兜底:某些会话的审查请求可能被记到了别的 sessionId 上。
|
|
983
|
-
// 只接受高置信匹配(参数完全一致或审查理由原样出现在审批理由里),且必须唯一。
|
|
984
|
-
let crossMatch = null
|
|
985
|
-
for (const [otherSession, otherList] of state.reviews) {
|
|
986
|
-
if (otherSession === sessionId || !Array.isArray(otherList)) continue
|
|
987
|
-
for (const review of otherList) {
|
|
988
|
-
if (review.claimedBy !== null || review.error !== null) continue
|
|
989
|
-
if (review.action === null || review.action.name !== toolName) continue
|
|
990
|
-
if (Date.now() - review.at > 120_000) continue
|
|
991
|
-
const reviewArgs = canonicalJson(normalizeArguments(review.action.arguments))
|
|
992
|
-
const sameArgs = execArgs !== undefined && reviewArgs.length > 0 && reviewArgs === execArgs
|
|
993
|
-
const reasonHit =
|
|
994
|
-
typeof reason === 'string' &&
|
|
995
|
-
typeof review.decision?.reason === 'string' &&
|
|
996
|
-
review.decision.reason.length >= 12 &&
|
|
997
|
-
reason.includes(review.decision.reason)
|
|
998
|
-
if (!sameArgs && !reasonHit) continue
|
|
999
|
-
if (crossMatch !== null) return null
|
|
1000
|
-
crossMatch = review
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
return crossMatch
|
|
990
|
+
return best
|
|
1004
991
|
}
|
|
1005
992
|
|
|
1006
993
|
/** pending 结束:记录结果、中止仍在流式输出的侧调用。 */
|
|
@@ -1025,6 +1012,16 @@ export function apply(ctx, config, runtime) {
|
|
|
1025
1012
|
/* ignore */
|
|
1026
1013
|
}
|
|
1027
1014
|
}
|
|
1015
|
+
// 审批结束:中止仍在进行的「重新评估」,避免审批结束后继续计费(安全审查 2026-11-03)。
|
|
1016
|
+
for (const candidate of [record.review, record.advisoryReview]) {
|
|
1017
|
+
const classifyController = candidate?.classifyController ?? null
|
|
1018
|
+
if (classifyController === null) continue
|
|
1019
|
+
try {
|
|
1020
|
+
classifyController.abort(new Error('approval resolved'))
|
|
1021
|
+
} catch {
|
|
1022
|
+
/* ignore */
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1028
1025
|
log.info(`[approval-consult] pending ${record.key} resolved outcome=${String(record.outcome)}`)
|
|
1029
1026
|
// 审批结束:立即失效该审批下的图片引用(字节仍由 DSH 附件服务保留,不在这里删)。
|
|
1030
1027
|
try {
|
|
@@ -1279,6 +1276,25 @@ export function apply(ctx, config, runtime) {
|
|
|
1279
1276
|
}
|
|
1280
1277
|
}
|
|
1281
1278
|
|
|
1279
|
+
/* 上面的 await 会让出执行权:必须在不再 await 的临界段里重新核对审批状态与并发名额,
|
|
1280
|
+
否则多个并发请求会一起通过数量检查(安全审查 2026-11-03)。 */
|
|
1281
|
+
if (record.resolvedAt !== null) {
|
|
1282
|
+
throw httpError(409, 'approval-resolved', '这次审批已经结束了,不能继续提问。')
|
|
1283
|
+
}
|
|
1284
|
+
if (target === 'reviewer') {
|
|
1285
|
+
const reviewAfterAwait = record.review ?? record.advisoryReview ?? null
|
|
1286
|
+
if (reviewAfterAwait === null || reviewAfterAwait.options === null || reviewAfterAwait.options === undefined) {
|
|
1287
|
+
throw httpError(409, 'reviewer-context-missing', '本次审批的审查材料已失效,请重新评估后再提问。')
|
|
1288
|
+
}
|
|
1289
|
+
if (reviewAfterAwait.advisory === true && reviewAfterAwait.decision === null) {
|
|
1290
|
+
throw httpError(409, 'review-not-initialized', '审查材料已就绪,但还没有重新评估。请先点「让审查 AI 重新评估」,再继续提问。')
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
const activeJobsAfterAwait = [...record.jobs].filter((jobId) => state.jobs.get(jobId)?.status === 'streaming').length
|
|
1294
|
+
if (activeJobsAfterAwait >= cfg.maxConcurrentJobs) {
|
|
1295
|
+
throw httpError(429, 'too-many-jobs', `同一审批下最多同时有 ${cfg.maxConcurrentJobs} 个回答在生成,请等其中一个结束。`)
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1282
1298
|
const entry = {
|
|
1283
1299
|
id: `q:${nextSeq()}`,
|
|
1284
1300
|
question,
|
|
@@ -1626,37 +1642,65 @@ export function apply(ctx, config, runtime) {
|
|
|
1626
1642
|
if (review.decision !== null) {
|
|
1627
1643
|
return { review, reused: true }
|
|
1628
1644
|
}
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
const options = {
|
|
1632
|
-
...review.options,
|
|
1633
|
-
maxTokens: 1800,
|
|
1634
|
-
signal: controller.signal,
|
|
1645
|
+
if (record.resolvedAt !== null && record.resolvedAt !== undefined) {
|
|
1646
|
+
throw httpError(409, 'approval-resolved', '这次审批已经结束了,不能继续重新评估。')
|
|
1635
1647
|
}
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
+
// 同一条记录只允许一个进行中的评估:重复请求共享同一个 Promise,避免重复计费与输出混合。
|
|
1649
|
+
if (review.classifyPromise !== undefined && review.classifyPromise !== null) {
|
|
1650
|
+
await review.classifyPromise
|
|
1651
|
+
return { review, reused: true }
|
|
1652
|
+
}
|
|
1653
|
+
const task = (async () => {
|
|
1654
|
+
const controller = new AbortController()
|
|
1655
|
+
const timer = setTimeout(() => controller.abort(new Error('advisory review timeout')), 90_000)
|
|
1656
|
+
review.classifyController = controller
|
|
1657
|
+
// 每次尝试使用独立缓冲:上一次失败留下的半截输出/错误不能污染本次结果。
|
|
1658
|
+
let text = ''
|
|
1659
|
+
let error = null
|
|
1660
|
+
let usage = null
|
|
1661
|
+
let finishKind = null
|
|
1662
|
+
const options = {
|
|
1663
|
+
...review.options,
|
|
1664
|
+
maxTokens: 1800,
|
|
1665
|
+
signal: controller.signal,
|
|
1648
1666
|
}
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1667
|
+
state.sideOptions.add(options)
|
|
1668
|
+
state.stats.sideCalls += 1
|
|
1669
|
+
try {
|
|
1670
|
+
for await (const chunk of ctx.llm.stream(options)) {
|
|
1671
|
+
if (!chunk || typeof chunk !== 'object') continue
|
|
1672
|
+
if (chunk.type === 'text-delta' && typeof chunk.text === 'string') text += chunk.text
|
|
1673
|
+
else if (chunk.type === 'block-end' && chunk.block?.type === 'text' && typeof chunk.block.text === 'string' && text.length === 0) text = chunk.block.text
|
|
1674
|
+
else if (chunk.type === 'usage') usage = chunk.usage ?? null
|
|
1675
|
+
else if (chunk.type === 'finish') {
|
|
1676
|
+
finishKind = chunk.reason?.kind ?? null
|
|
1677
|
+
if (chunk.reason?.kind === 'error' || chunk.reason?.kind === 'aborted') error = messageOf(chunk.reason.failure)
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
review.text = text
|
|
1681
|
+
review.decisionText = text.trim()
|
|
1682
|
+
review.decision = parseReviewDecision(review.decisionText)
|
|
1683
|
+
if (review.decision === null && error === null) {
|
|
1684
|
+
error = `审查输出无法解析:${clip(review.decisionText, 200)}`
|
|
1685
|
+
}
|
|
1686
|
+
} catch (thrown) {
|
|
1687
|
+
review.text = text
|
|
1688
|
+
error = friendlySideCallError(thrown)
|
|
1689
|
+
noteError('advisory-classify', thrown)
|
|
1690
|
+
} finally {
|
|
1691
|
+
review.error = error
|
|
1692
|
+
review.usage = usage
|
|
1693
|
+
review.finishKind = finishKind
|
|
1694
|
+
clearTimeout(timer)
|
|
1695
|
+
state.sideOptions.delete(options)
|
|
1696
|
+
review.classifyController = null
|
|
1653
1697
|
}
|
|
1654
|
-
}
|
|
1655
|
-
|
|
1656
|
-
|
|
1698
|
+
})()
|
|
1699
|
+
review.classifyPromise = task
|
|
1700
|
+
try {
|
|
1701
|
+
await task
|
|
1657
1702
|
} finally {
|
|
1658
|
-
|
|
1659
|
-
state.sideOptions.delete(options)
|
|
1703
|
+
review.classifyPromise = null
|
|
1660
1704
|
}
|
|
1661
1705
|
return { review, reused: false }
|
|
1662
1706
|
}
|
|
@@ -2455,6 +2499,11 @@ export function apply(ctx, config, runtime) {
|
|
|
2455
2499
|
if (item === undefined || item.consultKey !== consultKey || Date.now() > item.expiresAt) {
|
|
2456
2500
|
throw httpError(404, 'attachment-unknown', '这张图片已经失效,请重新上传。')
|
|
2457
2501
|
}
|
|
2502
|
+
// 审批结束后图片引用必须立即失效(finalizePending 会清理,这里兜底防止竞态残留)。
|
|
2503
|
+
const pendingForImage = state.pendings.get(consultKey)
|
|
2504
|
+
if (pendingForImage === undefined || pendingForImage.resolvedAt !== null) {
|
|
2505
|
+
throw httpError(404, 'attachment-unknown', '这张图片已经失效,请重新上传。')
|
|
2506
|
+
}
|
|
2458
2507
|
if (attachmentStore === null || typeof attachmentStore.readImage !== 'function') {
|
|
2459
2508
|
throw httpError(409, 'attachment-unavailable', '当前环境没有可用的附件服务。')
|
|
2460
2509
|
}
|
|
@@ -2487,20 +2536,35 @@ export function apply(ctx, config, runtime) {
|
|
|
2487
2536
|
}
|
|
2488
2537
|
const serviceLimit = Number(attachmentStore.imageLimits?.maxImageBytes)
|
|
2489
2538
|
const limit = Math.min(cfg.maxAttachmentBytes, Number.isFinite(serviceLimit) && serviceLimit > 0 ? serviceLimit : MAX_ATTACHMENT_BODY_BYTES)
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
let ref = null
|
|
2539
|
+
// 先占名额再读取/保存:超额或并发上传不会浪费宿主存储与解析资源。
|
|
2540
|
+
reserveAttachmentSlot(record.key)
|
|
2493
2541
|
try {
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2542
|
+
const data = await readBinaryBody(req, limit)
|
|
2543
|
+
if (data.length === 0) throw httpError(400, 'attachment-bad-request', '图片内容为空。')
|
|
2544
|
+
// 读取期间审批可能已经结束:结束后的图片一律不再登记(否则会“复活”已失效引用)。
|
|
2545
|
+
if (record.resolvedAt !== null) {
|
|
2546
|
+
throw httpError(409, 'approval-resolved', '这次审批已经结束,不能再上传图片。')
|
|
2547
|
+
}
|
|
2548
|
+
let ref = null
|
|
2549
|
+
try {
|
|
2550
|
+
ref = await attachmentStore.saveImage({
|
|
2551
|
+
data,
|
|
2552
|
+
mediaType,
|
|
2553
|
+
name: sanitizeAttachmentName(query.get('name')),
|
|
2554
|
+
})
|
|
2555
|
+
} catch (error) {
|
|
2556
|
+
throw mapAttachmentError(error)
|
|
2557
|
+
}
|
|
2558
|
+
// saveImage 也可能耗时:登记前再复核一次;若此时审批已结束,本次保存的宿主对象
|
|
2559
|
+
// 没有对应引用(宿主接口未提供删除语义),但绝不会被登记成可读取的附件。
|
|
2560
|
+
if (record.resolvedAt !== null) {
|
|
2561
|
+
throw httpError(409, 'approval-resolved', '这次审批已经结束,不能再上传图片。')
|
|
2562
|
+
}
|
|
2563
|
+
const item = registerAttachment(ref, record.key, record.sessionId)
|
|
2564
|
+
sendJson(res, 200, { ok: true, attachment: attachmentMeta(item) })
|
|
2565
|
+
} finally {
|
|
2566
|
+
releaseAttachmentSlot(record.key)
|
|
2501
2567
|
}
|
|
2502
|
-
const item = registerAttachment(ref, record.key, record.sessionId)
|
|
2503
|
-
sendJson(res, 200, { ok: true, attachment: attachmentMeta(item) })
|
|
2504
2568
|
}),
|
|
2505
2569
|
)
|
|
2506
2570
|
addDisposer(
|
|
@@ -3230,6 +3294,30 @@ export function apply(ctx, config, runtime) {
|
|
|
3230
3294
|
return out
|
|
3231
3295
|
}
|
|
3232
3296
|
|
|
3297
|
+
/** 该审批下进行中的上传预占数。 */
|
|
3298
|
+
function pendingAttachmentCount(consultKey) {
|
|
3299
|
+
return state.attachmentPending.get(consultKey) ?? 0
|
|
3300
|
+
}
|
|
3301
|
+
|
|
3302
|
+
/**
|
|
3303
|
+
* 上传前预占名额:超额/并发上传不会先 saveImage 再被拒(安全审查 2026-11-03)。
|
|
3304
|
+
* 调用方必须在 finally 里 releaseAttachmentSlot。
|
|
3305
|
+
*/
|
|
3306
|
+
function reserveAttachmentSlot(consultKey) {
|
|
3307
|
+
const existing = attachmentsForConsult(consultKey).length
|
|
3308
|
+
const pending = pendingAttachmentCount(consultKey)
|
|
3309
|
+
if (existing + pending >= cfg.maxAttachmentsPerConsult) {
|
|
3310
|
+
throw httpError(413, 'attachment-too-many', `同一审批下最多保留 ${cfg.maxAttachmentsPerConsult} 张图片。`)
|
|
3311
|
+
}
|
|
3312
|
+
state.attachmentPending.set(consultKey, pending + 1)
|
|
3313
|
+
}
|
|
3314
|
+
|
|
3315
|
+
function releaseAttachmentSlot(consultKey) {
|
|
3316
|
+
const pending = pendingAttachmentCount(consultKey)
|
|
3317
|
+
if (pending <= 1) state.attachmentPending.delete(consultKey)
|
|
3318
|
+
else state.attachmentPending.set(consultKey, pending - 1)
|
|
3319
|
+
}
|
|
3320
|
+
|
|
3233
3321
|
/** 登记一张已存入附件服务的图片,返回对外展示的元数据。 */
|
|
3234
3322
|
function registerAttachment(ref, consultKey, sessionId) {
|
|
3235
3323
|
const existing = attachmentsForConsult(consultKey)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-approval-consult",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Approval consultation panel for DeepSeek Harness: while a tool call waits for approval, ask the executor AI and the auto-review AI independently, in two isolated channels with prompt-cache reuse.",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"NOTICE.md"
|
|
35
35
|
],
|
|
36
36
|
"scripts": {
|
|
37
|
-
"test": "node tests/unit.mjs && node tests/locale-parity.mjs && node tests/css-classes.mjs",
|
|
37
|
+
"test": "node tests/unit.mjs && node tests/locale-parity.mjs && node tests/css-classes.mjs && node tests/security.mjs",
|
|
38
38
|
"check": "node --check lib/impl.js && node --check lib/client.js && node --check lib/index.js"
|
|
39
39
|
},
|
|
40
40
|
"engines": {
|