progmune-runtime 3.7.51 → 3.7.53
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 +57 -0
- package/dist/extract-ir-taint-structural.test.js +78 -4
- package/dist/extract-ir.js +214 -0
- package/dist/protocol-detector-acronym.test.js +118 -0
- package/dist/protocol-detector-authz-drift.test.js +65 -0
- package/dist/protocol-detector-authz-machinery.test.js +83 -0
- package/dist/protocol-detector-authz-selfpred.test.js +105 -0
- package/dist/protocol-detector-authz.test.js +78 -0
- package/dist/protocol-detector-credential-issuance.test.js +64 -0
- package/dist/protocol-detector-owner-self.test.js +57 -0
- package/dist/protocol-detector-paramgate.test.js +105 -0
- package/dist/protocol-detector.js +331 -17
- package/dist/sdk.js +1 -1
- package/package.json +2 -2
|
@@ -6,7 +6,12 @@
|
|
|
6
6
|
* any project's naming conventions (curl, nginx, redis, any C project).
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.IDENTITY_PARAM_RE = exports.AUTHZ_BARE_CAN_RE = exports.AUTHZ_ACCESS_CHECK_RE = exports.AUTHZ_CAN_HELPER_RE = exports.AUTHZ_PREDICATE_RE = exports.AUTHZ_ACTION_WORD = void 0;
|
|
9
10
|
exports.identifierParse = identifierParse;
|
|
11
|
+
exports.tokenAcronyms = tokenAcronyms;
|
|
12
|
+
exports.isAuthDecisionName = isAuthDecisionName;
|
|
13
|
+
exports.isAuthFunctionName = isAuthFunctionName;
|
|
14
|
+
exports.classifyParamType = classifyParamType;
|
|
10
15
|
exports.detectSafeguardViolations = detectSafeguardViolations;
|
|
11
16
|
exports.buildCallerMap = buildCallerMap;
|
|
12
17
|
exports.detectSafeguardViolationsV7 = detectSafeguardViolationsV7;
|
|
@@ -267,6 +272,48 @@ const PLSB_PROTOCOLS = [
|
|
|
267
272
|
},
|
|
268
273
|
];
|
|
269
274
|
const ALL_PROTOCOLS = [...PROTOCOLS, ...PLSB_PROTOCOLS];
|
|
275
|
+
/**
|
|
276
|
+
* §38(2026-09-25)组合式授权谓语 —— 三条,逐 call 名匹配(safeguard 口径)。
|
|
277
|
+
*
|
|
278
|
+
* 为什么不是继续往白名单里加词:现有 `canModify|canDelete|canEdit` 是**枚举**,
|
|
279
|
+
* 而真实谓语是「can/cannot/may + 任意授权动作」的**组合**,枚举永远追不上
|
|
280
|
+
* (实测 docmost 全量 136 条 trigger 命中里,旧白名单抑制 **0** 条)。
|
|
281
|
+
*
|
|
282
|
+
* ⚠ 三个踩过的假阳性(都是实测抓到的,改之前先读 §38.4):
|
|
283
|
+
* ① `cannot` 必须单列 —— 塞进 `[A-Z_]\w*` 后缀组会让它自己都不匹配。
|
|
284
|
+
* ② `can<X>` 的 X 必须是**授权动作词**:`rateLimiter.canSendEmail(userId)` 是限流器,
|
|
285
|
+
* 不是权限判定 ⇒ 动作词表刻意不含 Send/Emit/Notify/Render/Parse。
|
|
286
|
+
* ③ `...Access` 类词尾必须**限定前缀是校验动词**:`deleteByUsersWithoutSpaceAccess`
|
|
287
|
+
* 是数据删除不是校验;且词尾不得含 `Rights?`(会命中 fp-ts 的 `E.isRight(x)`)。
|
|
288
|
+
*/
|
|
289
|
+
exports.AUTHZ_ACTION_WORD = "(?:Manage|Read|Write|Create|Update|Delete|Edit|Remove|View|Share|Transfer|Invite|Comment|Move|Rename|Publish|Access|Modify|Upload|Download|Export|Import|Lock|Unlock|Restore|Archive|Pin|Unpin|Assign|Approve|Reject|Execute|Grant|Revoke|All|Any)";
|
|
290
|
+
/** A. CASL 谓语:cannot( / canRemove( / canPublish( / mayCreate( */
|
|
291
|
+
exports.AUTHZ_PREDICATE_RE = new RegExp("^(?:cannot|can_not|isUnable|isDenied|isForbidden)$" +
|
|
292
|
+
"|^(?:can|able|may|isAble|isAllowed|isPermitted|hasPermission|checkPermission|authorize|isAuthorized|authorised|allowed|permitted)" +
|
|
293
|
+
exports.AUTHZ_ACTION_WORD, "i");
|
|
294
|
+
/** A′. 带前缀 helper:validateCanEdit / assertCanDelete / isSharingAllowedFor */
|
|
295
|
+
exports.AUTHZ_CAN_HELPER_RE = /^\w*(?:Can|May|Allowed|Permitted|Authorized|Authorised)[A-Z]\w+$/;
|
|
296
|
+
/** A″. 访问/所有权校验:validateSpaceAccess / assertOwnership / checkMediaDeletePermission */
|
|
297
|
+
exports.AUTHZ_ACCESS_CHECK_RE = /^(?:validate|assert|check|ensure|verify|require|confirm|test|has|can|must|needs?)[A-Z_]?\w*(?:Access|Ownership|Permission|Authorization|Grant)\w*$/i;
|
|
298
|
+
/**
|
|
299
|
+
* §40 C1:裸授权谓语 —— `can('publish')` / `cannot(...)` / `ability.can(...)`。
|
|
300
|
+
*
|
|
301
|
+
* 为什么 §38 那条收不到:AUTHZ_PREDICATE_RE 要求 `can` 后面**紧跟动作词**
|
|
302
|
+
* (canRemove / canPublish),而真实世界的两种主流写法都是**裸** can:
|
|
303
|
+
* - CASL:`ability.can(Action.Update, subject)` ← 动作是**实参**,不在名字里
|
|
304
|
+
* - verdaccio:`const can = allow(auth, …)`;路由上 `can('publish')`
|
|
305
|
+
* ⇒ 这两种在 calls 里留下的名字就是 `can` / `ability.can`。
|
|
306
|
+
*
|
|
307
|
+
* ⚠⚠ 必须**精确**匹配(`^can$` / `.can$`),绝不能做成 `/\bcan\b/`:
|
|
308
|
+
* cancel / candidate / canary / cancan 全都同形但跟授权无关(fixture 已钉死)。
|
|
309
|
+
*/
|
|
310
|
+
exports.AUTHZ_BARE_CAN_RE = /^(?:can|cannot)$|\.(?:can|cannot)$/i;
|
|
311
|
+
/**
|
|
312
|
+
* ⚠⚠ 挂到规则上时必须带 `callsOnly: true`(2026-09-25 §40 实测踩到):
|
|
313
|
+
* 默认会在 `effectiveCalls` 上匹配,而 effectiveCalls 含 **identifierParse 拆出的词**
|
|
314
|
+
* ⇒ `canSendEmail` 会拆出单词 "can" ⇒ 裸 can 误命中 ⇒ §38 的负对照直接转绿。
|
|
315
|
+
* callsOnly 把匹配限定在**原始调用名**上(`can` 作为被调名才作数)。
|
|
316
|
+
*/
|
|
270
317
|
const SAFEGUARD_RULES = [
|
|
271
318
|
// ── Password Hashing ──
|
|
272
319
|
{
|
|
@@ -322,6 +369,20 @@ const SAFEGUARD_RULES = [
|
|
|
322
369
|
{ pattern: /\b(checkOwner|isOwner|ownerId\s*[!=]==?|authorId\s*[!=]==?|userId\s*[!=]==?|createdBy\s*[!=]==?|\.owner\s*[!=]==?|\.user\s*[!=]==?)\b/i, label: "ownership_check" },
|
|
323
370
|
{ pattern: /\b(hasPermission|checkPermission|checkAccess|isAuthorized|checkRole|requireRole|adminCheck|isAdmin|canModify|canDelete|canEdit)\b/i, label: "authz_check" },
|
|
324
371
|
{ pattern: /\b(__progmune_ownership_checked__)\b/, label: "inline_ownership_check" },
|
|
372
|
+
// §46 ②:资源属主被**赋值为当前主体**(`{ userId: user.id }` / `where:{userId: user.id}`)。
|
|
373
|
+
// 这类函数的被操作对象就是(或已被限定在)主体自己,谈不上「改别人的东西」。
|
|
374
|
+
// 合成语料上覆盖 10/10 FP(createTransfer / createRefund / uploadFile / placeOrder…)。
|
|
375
|
+
// ⚠ 只认 `<属主字段>: <主体>.id` 的**绑定**形态;**不认** `<调用>(…, user.id, …)` ——
|
|
376
|
+
// 主体标识作实参这一形状实测存在相反真值(deletePAT 是 FP,deleteWidget 是 TP),按 R66 不落地。
|
|
377
|
+
{ pattern: /\b(__progmune_owner_self_assigned__)\b/, label: "owner_self_assigned" },
|
|
378
|
+
// §38(2026-09-25)CASL / 组合式授权谓语。上面两条是**动词白名单**,
|
|
379
|
+
// 而真实世界的谓语是 `can|cannot|may` + 动作 的**组合式**,
|
|
380
|
+
// 白名单穷举不完(canRemove/canManage/canPublish…)。三条规则见 §38.3。
|
|
381
|
+
{ pattern: exports.AUTHZ_PREDICATE_RE, label: "casl_predicate" },
|
|
382
|
+
{ pattern: exports.AUTHZ_CAN_HELPER_RE, label: "can_helper" },
|
|
383
|
+
{ pattern: exports.AUTHZ_ACCESS_CHECK_RE, label: "access_check" },
|
|
384
|
+
// §40 C1:裸 can(CASL ability.can / verdaccio can('publish'))—— 精确匹配
|
|
385
|
+
{ pattern: exports.AUTHZ_BARE_CAN_RE, label: "bare_can", callsOnly: true },
|
|
325
386
|
],
|
|
326
387
|
violationMessage: "Mutation operation does not verify that the acting user owns the resource or holds the required permission before modifying data.",
|
|
327
388
|
conceptMissing: ["OwnershipCheck", "AuthorizationGuard"],
|
|
@@ -348,7 +409,9 @@ const SAFEGUARD_RULES = [
|
|
|
348
409
|
paramGated: true,
|
|
349
410
|
trigger: /\b(list|download|view|fetch)(?:[A-Z]\w*|_\w+)|get(?:[A-Z]\w+|_\w+)/i,
|
|
350
411
|
safeguards: [
|
|
351
|
-
{ pattern: /\b(getUser|validateToken|verifySession|getSessionUser|getCurrentUser|token\w*(Check|Verify|Valid)|session\w*(Check|Verify|Valid)|auth\w*(Check|Verify|Valid|Guard|Middleware|Required)|requireAuth|withAuth|authenticate\w*(User|Request|Token)?|checkAuth|isAuth|hasAuth|checkAccess|hasAccess|get_user|get_session_user|get_current_user|validate_session|verify_token|require_auth|with_auth|check_auth|auth_required|authenticate_user|authenticate_request|authenticate_token|token_check|token_verify|token_valid|session_check|session_verify|session_valid|auth_check|auth_guard|auth_middleware|get_current_user_authorizer|current_user_authorizer|login_required|permission_required|user_passes_test|check_authorization|check_permission|jwt\.decode|decode_token|__progmune_auth_checked__|__progmune_credential_check__|__progmune_drf_permissions__|__progmune_auth_machinery__)\b/i, label: "auth_check" },
|
|
412
|
+
{ pattern: /\b(getUser|validateToken|verifyToken|verifySession|validateSession|getSessionUser|getSession\b|getCurrentUser|token\w*(Check|Verify|Valid)|session\w*(Check|Verify|Valid)|auth\w*(Check|Verify|Valid|Guard|Middleware|Required)|requireAuth|withAuth|authenticate\w*(User|Request|Token)?|checkAuth|isAuth|hasAuth|checkAccess|hasAccess|get_user|get_session_user|get_current_user|validate_session|verify_token|require_auth|with_auth|check_auth|auth_required|authenticate_user|authenticate_request|authenticate_token|token_check|token_verify|token_valid|session_check|session_verify|session_valid|auth_check|auth_guard|auth_middleware|get_current_user_authorizer|current_user_authorizer|login_required|permission_required|user_passes_test|check_authorization|check_permission|jwt\.decode|decode_token|__progmune_auth_checked__|__progmune_credential_check__|__progmune_drf_permissions__|__progmune_auth_machinery__)\b/i, label: "auth_check" },
|
|
413
|
+
// §40 C1:裸 can —— verdaccio 的 can('publish') 中间件就是这里的授权判定
|
|
414
|
+
{ pattern: exports.AUTHZ_BARE_CAN_RE, label: "bare_can", callsOnly: true },
|
|
352
415
|
],
|
|
353
416
|
violationMessage: "Data access function does not check authentication. Anyone can access data without credentials.",
|
|
354
417
|
conceptMissing: ["AuthenticationCheck", "AccessControl"],
|
|
@@ -380,7 +443,19 @@ const SAFEGUARD_RULES = [
|
|
|
380
443
|
// (listPosts/getPost/deletePost fire via identifier-parsed words).
|
|
381
444
|
trigger: /\b(add|create|update|set|publish|insert|submit)(?:[A-Z]\w*|_\w+)|(?:[A-Z]\w*|_\w+)(Add|Create|Update|Set|Publish|Insert|Submit)\b/i,
|
|
382
445
|
safeguards: [
|
|
383
|
-
{ pattern: /\b(getUser|validateToken|verifyToken|verifySession|validateSession|getSessionUser|getSession\b|getCurrentUser|token\w*(Check|Verify|Valid)|session\w*(Check|Verify|Valid)|auth\w*(Check|Verify|Valid|Guard|Middleware|Required)|requireAuth|withAuth|authenticate\w*(User|Request|Token)?|checkAuth|isAuth|hasAuth|checkAccess|hasAccess|get_user|get_session_user|get_current_user|validate_session|verify_token|require_auth|with_auth|check_auth|auth_required|authenticate_user|authenticate_request|authenticate_token|token_check|token_verify|token_valid|session_check|session_verify|session_valid|auth_check|auth_guard|auth_middleware|get_current_user_authorizer|current_user_authorizer|login_required|permission_required|user_passes_test|check_authorization|check_permission|
|
|
446
|
+
{ pattern: /\b(getUser|validateToken|verifyToken|verifySession|validateSession|getSessionUser|getSession\b|getCurrentUser|token\w*(Check|Verify|Valid)|session\w*(Check|Verify|Valid)|auth\w*(Check|Verify|Valid|Guard|Middleware|Required)|requireAuth|withAuth|authenticate\w*(User|Request|Token)?|checkAuth|isAuth|hasAuth|checkAccess|hasAccess|get_user|get_session_user|get_current_user|validate_session|verify_token|require_auth|with_auth|check_auth|auth_required|authenticate_user|authenticate_request|authenticate_token|token_check|token_verify|token_valid|session_check|session_verify|session_valid|auth_check|auth_guard|auth_middleware|get_current_user_authorizer|current_user_authorizer|login_required|permission_required|user_passes_test|check_authorization|check_permission|__progmune_auth_checked__|__progmune_credential_check__|__progmune_drf_permissions__|__progmune_auth_machinery__)\b/i, label: "auth_check" },
|
|
447
|
+
// §40 C1:裸 can —— 同上
|
|
448
|
+
{ pattern: exports.AUTHZ_BARE_CAN_RE, label: "bare_can", callsOnly: true },
|
|
449
|
+
// §46 ①:凭签发入口豁免 —— 独立 safeguard,**不并入 auth_check**。
|
|
450
|
+
// create_*_token 是「签发令牌」不是「校验调用者身份」,两者语义相反:
|
|
451
|
+
// 调用 create_access_token 恰恰说明该函数**没有**校验任何人的身份(它就是发令牌的地方)。
|
|
452
|
+
// 之所以仍要豁免,是因为 login / register / issueToken 这类入口天然不需要事前认证
|
|
453
|
+
// (这里压的是「登录接口被判未认证变更」那批 FP),不是因为它做了认证。
|
|
454
|
+
// ⚠ 因此它**不得对称补进 Unauthenticated Access**(R66:同形状存在相反真值 ——
|
|
455
|
+
// getRefreshToken(userId) 是该免的签发入口,getUserToken(userId) 是不该免的越权读取,
|
|
456
|
+
// 单函数视角区分不了)。分开成两条 label 就是为了让下一次词表 diff(R69)
|
|
457
|
+
// 不再把这三个词当成 auth_check 的「缺项」而机械对齐过去。
|
|
458
|
+
{ pattern: /\b(create_access_token|create_refresh_token|create_jwt_token)\b/i, label: "credential_issuance" },
|
|
384
459
|
],
|
|
385
460
|
violationMessage: "Mutation function does not check authentication. Anyone can create or modify data without credentials.",
|
|
386
461
|
conceptMissing: ["AuthenticationCheck", "AccessControl"],
|
|
@@ -437,12 +512,38 @@ const SAFEGUARD_RULES = [
|
|
|
437
512
|
// 175 条损失已逐条归类,无一为真阳性。⇒ trigger 只看函数名。
|
|
438
513
|
triggerOwnNameOnly: true,
|
|
439
514
|
trigger: /\b(create|add|post|upload)(?:[A-Z]\w*|_\w+)|(?:[A-Z]\w*|_\w+)(Create|Add|Post|Upload)\b/i,
|
|
515
|
+
// E5(2026-09-23):语义前置条件 —— 只有「参数确实流向外部副作用」才谈得上
|
|
516
|
+
// 该不该校验。此前 trigger 是纯函数名正则(create|add|post|upload),判据手里
|
|
517
|
+
// **只有名字**:verdaccio 的 ConfigBuilder.addStorage/addLogger(纯配置装配)、
|
|
518
|
+
// ducktors 的 createS3/createAzureBlobStorage(工厂)都被要求「必须校验输入」,
|
|
519
|
+
// 而它们根本不接外部输入。FP 池实测:75 条里 62 条(83%)完全没有请求入口迹象。
|
|
520
|
+
//
|
|
521
|
+
// 为什么是「副作用」而不是「是否 HTTP 入口」(R41:先分缺信息 vs 缺语义):
|
|
522
|
+
// 「非入口 ⇒ 不报」这个更直觉的判据**有毒** —— 真值集里 4 条已核验 TP
|
|
523
|
+
// (docmost addContributors→redis.sadd、addLabelsToPage→labelRepo.findOrCreate、
|
|
524
|
+
// hedgedoc ApiTokenService.createToken、hoppscotch AdminService.createATeam)
|
|
525
|
+
// 都没有 req 对象,但参数是从 Controller 透传的外部输入,且确实没有校验。
|
|
526
|
+
// ⇒ Service 层方法不因「组件边界」获得豁免。区分点在**参数流向**,不在入口形态。
|
|
527
|
+
//
|
|
528
|
+
// 量化(blind-benchmark/probe-body-evidence.ts --from-gold,74 条已标注真值):
|
|
529
|
+
// TP 召回 100%(17/17,丢 0 条真报)/FP 压制 77.2%(44/57)/
|
|
530
|
+
// precision 23.0% → 56.7%/净收益 44 条。多数类基线 77.0%。
|
|
531
|
+
// 「要求副作用调用吃到参数」的精化版 FP 压制虽达 91.2%,但会丢 4 条真报,
|
|
532
|
+
// 漏报比误报贵 ⇒ 取粗判据。
|
|
533
|
+
requireMarker: "__progmune_input_effect__",
|
|
534
|
+
// 该标记只由 TS 提取器产出(Python 提取器不产出任何 __progmune_* 标记),
|
|
535
|
+
// 故只在 TS/JS 上强制,Python 侧保持原有行为,避免静默归零(R42)。
|
|
536
|
+
requireMarkerLanguages: ["typescript", "javascript"],
|
|
440
537
|
safeguards: [
|
|
441
538
|
// E3(2026-09-22):__progmune_input_schema__ —— 提取器在「入参类型是带校验
|
|
442
539
|
// 装饰器的 DTO 类」时产出(NestJS 把校验写在 DTO 属性装饰器上,词形匹配永远
|
|
443
540
|
// 看不到)。本规则此前不接受任何 __progmune_* 标记 ⇒ 该证据通道是断的。
|
|
444
541
|
// 注意只加在**抑制位**(safeguard),不进 trigger:新通道只能做减法。
|
|
445
|
-
|
|
542
|
+
// E4(2026-09-23):__progmune_input_guard__ —— 提取器在函数体内找到输入校验
|
|
543
|
+
// 证据时产出(limits:{fileSize…} / if(!x) throw BadRequest…)。此前规则只看
|
|
544
|
+
// 函数名与调用名,函数体里的校验看不见 ⇒ docmost 两个 upload controller
|
|
545
|
+
// 明明有校验仍被判「未校验」。与 E3 同源:只加在抑制位,不进 trigger。
|
|
546
|
+
{ pattern: /\b(validate|sanitize|check|verify)(?:[A-Z]\w*|_\w*)(Content|Input|Length|Title|Body|Type|Size|File|Data|Param|Arg|Field|Value)\b|\b(validateContent|sanitizeInput|checkLength|verifyType|checkSize)\b|__progmune_input_schema__|__progmune_input_guard__/i, label: "input_validation" },
|
|
446
547
|
],
|
|
447
548
|
violationMessage: "Content creation function does not validate or sanitize input. XSS, injection, and oversized content possible.",
|
|
448
549
|
conceptMissing: ["InputSanitization", "ContentValidation", "SizeLimit"],
|
|
@@ -502,6 +603,12 @@ const SAFEGUARD_RULES = [
|
|
|
502
603
|
safeguards: [
|
|
503
604
|
{ pattern: /\b(ownerId\s*[!=]==?|authorId\s*[!=]==?|userId\s*[!=]==?|createdBy|\.owner\s*[!=]==?)/i, label: "ownership_comparison" },
|
|
504
605
|
{ pattern: /\b(__progmune_ownership_checked__)\b/, label: "inline_ownership_check" },
|
|
606
|
+
// §38:同 Ownership Check,组合式谓语一并接上(这条规则更严,更需要)
|
|
607
|
+
{ pattern: exports.AUTHZ_PREDICATE_RE, label: "casl_predicate" },
|
|
608
|
+
{ pattern: exports.AUTHZ_CAN_HELPER_RE, label: "can_helper" },
|
|
609
|
+
{ pattern: exports.AUTHZ_ACCESS_CHECK_RE, label: "access_check" },
|
|
610
|
+
// §40 C1:同上
|
|
611
|
+
{ pattern: exports.AUTHZ_BARE_CAN_RE, label: "bare_can", callsOnly: true },
|
|
505
612
|
],
|
|
506
613
|
violationMessage: "Resource mutation checks authentication but does NOT verify the resource belongs to the requesting user. Missing ownerId/authorId comparison.",
|
|
507
614
|
conceptMissing: ["ResourceOwnership", "HorizontalAuthorization"],
|
|
@@ -1104,7 +1211,7 @@ function identifierParse(name) {
|
|
|
1104
1211
|
const words = [];
|
|
1105
1212
|
for (const part of parts) {
|
|
1106
1213
|
// Split camelCase/PascalCase: "registerNewUser" → ["register", "New", "User"]
|
|
1107
|
-
const camelWords = part.replace(/([a-z])([A-Z])/g, "$1
|
|
1214
|
+
const camelWords = part.replace(/([a-z])([A-Z])/g, "$1\x00$2").split("\x00");
|
|
1108
1215
|
for (const w of camelWords) {
|
|
1109
1216
|
if (w.length > 0)
|
|
1110
1217
|
words.push(w);
|
|
@@ -1112,11 +1219,200 @@ function identifierParse(name) {
|
|
|
1112
1219
|
}
|
|
1113
1220
|
return words;
|
|
1114
1221
|
}
|
|
1222
|
+
/**
|
|
1223
|
+
* §39(2026-09-25)D 类自指排除:把「鉴权机制自身」也识别为 auth function。
|
|
1224
|
+
*
|
|
1225
|
+
* 引擎原有 AUTH_PATTERN 只列了**用户鉴权入口**(login / register / logout / …),
|
|
1226
|
+
* 没列**鉴权机制自身**,于是对下面这类函数报「未鉴权」,属自指谬误:
|
|
1227
|
+
* JwtAuthGuard.handleRequest / TokenService.verifyJwt / AuthService.getCollabToken
|
|
1228
|
+
* / WorkspaceAbilityFactory.createForUser(CASL 授权引擎本体)
|
|
1229
|
+
*
|
|
1230
|
+
* 判据是**结构化**的两条,不是再往里加词(R55:组合规则必须带约束):
|
|
1231
|
+
*
|
|
1232
|
+
* ① 机制类容器 = 机制后缀(Guard|Strategy|AbilityFactory) **×** 凭据名词
|
|
1233
|
+
* ⚠ 只看后缀不够:`Strategy` 是通用设计模式后缀,CacheStrategy /
|
|
1234
|
+
* PaymentStrategy 会被误吃;`Guard` 同理(NestJS 的 ThrottlerGuard 是
|
|
1235
|
+
* 限流不是鉴权)。加凭据名词约束后这两类都排除。
|
|
1236
|
+
* ② 方法 = 机制动词 × 机制对象,**只看点号后的方法部分**
|
|
1237
|
+
* (容器词不参与动词匹配,否则 ApiTokensController 会漏进来)
|
|
1238
|
+
*
|
|
1239
|
+
* ⚠⚠ 边界(实测踩到的越界,别再放宽):
|
|
1240
|
+
* - **不得**改成「容器含 token/session 即算」。实测那样会吃掉
|
|
1241
|
+
* `ApiTokensController.deleteToken` / `AccessTokenController.deletePAT` /
|
|
1242
|
+
* `AccessTokenService.updateLastUsedForPAT` 的 Ownership Check ——
|
|
1243
|
+
* 对**凭据做增删改**恰恰是最需要鉴权的地方,压掉就是真漏报且不可见。
|
|
1244
|
+
* - 机制对象**不含** password(changePassword/passwordReset 是受保护操作)
|
|
1245
|
+
* - 机制对象**不含** pat(hoppscotch 产品专有缩写,收它会吃掉 createPAT,
|
|
1246
|
+
* 属过拟合到单个仓库的缩写)
|
|
1247
|
+
*/
|
|
1248
|
+
const AUTH_MACHINERY_CLASS_SUFFIX = /(?:Guard|Strategy|AbilityFactory)$/;
|
|
1249
|
+
const AUTH_MACHINERY_NOUN_WORDS = [
|
|
1250
|
+
"auth", "jwt", "token", "session", "cookie", "credential", "passport",
|
|
1251
|
+
"identity", "oauth", "oidc", "ldap", "saml", "bearer", "apikey", "ability", "permission",
|
|
1252
|
+
];
|
|
1253
|
+
const AUTH_MACHINERY_VERB_WORDS = [
|
|
1254
|
+
"verify", "validate", "sign", "generate", "issue", "decode", "parse",
|
|
1255
|
+
"encode", "refresh", "rotate", "get", "set", "create", "build", "make", "write", "read",
|
|
1256
|
+
];
|
|
1257
|
+
const AUTH_MACHINERY_OBJECT_WORDS = [
|
|
1258
|
+
"jwt", "token", "cookie", "session", "credential", "credentials", "signature", "bearer",
|
|
1259
|
+
];
|
|
1260
|
+
/**
|
|
1261
|
+
* §41 E1:token **内嵌**的 acronym 前缀 —— JWT from `JWTPayload`。
|
|
1262
|
+
*
|
|
1263
|
+
* 背景(§40.8 候选 C5):`identifierParse("verifyJWTPayload")` → ["verify","JWTPayload"],
|
|
1264
|
+
* "JWTPayload" 整块 ≠ "jwt" ⇒ 凭据语义对**任何**词表都不可见。
|
|
1265
|
+
*
|
|
1266
|
+
* ⚠⚠ 为什么**不改 identifierParse 主干**(C5 原案的处置结论,2026-09-25):
|
|
1267
|
+
* identifierParse 有 **5 个调用点**,其中 `triggerParsedWords` 供给 **trigger 匹配**、
|
|
1268
|
+
* `effectiveCalls` 供给 **safeguard 匹配** —— 动它等于同时改所有规则的触发面与抑制面。
|
|
1269
|
+
* `split-probe.ts` 实测:母本 803 个去重标识符里只有 **16 个**受影响,新增 22 个词中
|
|
1270
|
+
* 仅 **2 个**(JWT / OIDC)是机制类,其余全是噪声:
|
|
1271
|
+
* OAuthToken → +"Auth" (从 OAuth 里拆出 Auth,污染面极大)
|
|
1272
|
+
* throwHTTPErr → +"HTTP" +"Err"
|
|
1273
|
+
* createATeam → +"A" +"Team"
|
|
1274
|
+
* prosemirrorNodeToYElement → +"Y" +"Element"
|
|
1275
|
+
* ⇒ 收益 2 条 vs 全局 trigger/effectiveCalls 漂移 ⇒ **主干不改**。
|
|
1276
|
+
*
|
|
1277
|
+
* 折中:**只在「这个函数是否是鉴权机制」这一处**补认 acronym ——
|
|
1278
|
+
* 局部 ⇒ 不影响 triggerParsedWords / effectiveCalls ⇒ **结构上不可能造成 LOST**。
|
|
1279
|
+
*/
|
|
1280
|
+
const ACRONYM_PREFIX = /^([A-Z]{2,})(?=[A-Z][a-z])/;
|
|
1281
|
+
/**
|
|
1282
|
+
* 单个 token 里嵌的 acronym:JWTPayload→JWT / OIDCStrategy→OIDC / HTTPErr→HTTP。
|
|
1283
|
+
* ⚠ `{2,}` 与贪婪回溯两条约束是实测出来的,别随手放宽:
|
|
1284
|
+
* - `{2,}`:否则 `OAuth` → "O"(satisfies O|Auth 的 lookahead),单字母进词表 = 灾难;
|
|
1285
|
+
* - 贪婪回溯:`OIDCStrategy` 必须拿到 "OIDC" 而不是 "OID"
|
|
1286
|
+
* (非贪婪 `[A-Z]+?` 会停在 OID,"oid" 不在任何机制词表里 ⇒ 这条救不了)。
|
|
1287
|
+
* - lookahead `[A-Z][a-z]`:纯 PascalCase 词(Token/Payload)不匹配,避免把自己再切一遍。
|
|
1288
|
+
*/
|
|
1289
|
+
function tokenAcronyms(token) {
|
|
1290
|
+
const m = ACRONYM_PREFIX.exec(token);
|
|
1291
|
+
return m ? [m[1]] : [];
|
|
1292
|
+
}
|
|
1293
|
+
/** identifierParse 的词 + 每个词内嵌的 acronym,统一小写 */
|
|
1294
|
+
function machineryWords(method) {
|
|
1295
|
+
const base = identifierParse(method);
|
|
1296
|
+
const out = base.map((w) => w.toLowerCase());
|
|
1297
|
+
for (const t of base)
|
|
1298
|
+
for (const a of tokenAcronyms(t))
|
|
1299
|
+
out.push(a.toLowerCase());
|
|
1300
|
+
return out;
|
|
1301
|
+
}
|
|
1302
|
+
function isAuthMachineryName(enclosingFuncName) {
|
|
1303
|
+
const dot = enclosingFuncName.lastIndexOf(".");
|
|
1304
|
+
const container = dot > 0 ? enclosingFuncName.slice(0, dot) : "";
|
|
1305
|
+
// ① 机制类容器
|
|
1306
|
+
if (container && AUTH_MACHINERY_CLASS_SUFFIX.test(container)) {
|
|
1307
|
+
const cl = container.toLowerCase();
|
|
1308
|
+
if (AUTH_MACHINERY_NOUN_WORDS.some((w) => cl.includes(w)))
|
|
1309
|
+
return true;
|
|
1310
|
+
}
|
|
1311
|
+
// ② 方法 = 机制动词 × 机制对象(§41 E1:对象侧支持内嵌 acronym,见 ACRONYM_PREFIX 注释)
|
|
1312
|
+
const method = dot >= 0 ? enclosingFuncName.slice(dot + 1) : enclosingFuncName;
|
|
1313
|
+
const mw = machineryWords(method);
|
|
1314
|
+
if (mw.some((w) => AUTH_MACHINERY_VERB_WORDS.includes(w)) &&
|
|
1315
|
+
mw.some((w) => AUTH_MACHINERY_OBJECT_WORDS.includes(w)))
|
|
1316
|
+
return true;
|
|
1317
|
+
return false;
|
|
1318
|
+
}
|
|
1319
|
+
/**
|
|
1320
|
+
* §40 C4:**容器严格等于裸鉴权类名** × **方法是授权决策或机制内部**。
|
|
1321
|
+
*
|
|
1322
|
+
* §39.8 记下的未收项:verdaccio `Auth.allow_publish` / `Auth.allow_access` /
|
|
1323
|
+
* `Auth.setLegacyAuthCacheEntry`。容器 `Auth` 不带 Guard/Strategy/AbilityFactory 后缀,
|
|
1324
|
+
* 方法也不是「机制动词 × 机制对象」 ⇒ §39 的结构化判据够不着。
|
|
1325
|
+
*
|
|
1326
|
+
* ⚠⚠ 两层约束**缺一不可**(宽变体实测,见 §40.5):
|
|
1327
|
+
* ① 容器必须**严格等于** Auth / Authorization / AccessControl / Permissions。
|
|
1328
|
+
* 放宽成 `Auth*` 会吃掉 `AuthController.deletePendingUserData` —— §39 的 V-D
|
|
1329
|
+
* 反向验证早就证明这类**不能**压(对凭据/待处理用户做增删改最需要鉴权)。
|
|
1330
|
+
* ② 方法必须是 `allow_*`(授权决策)或含机制名词(Cache/Token/JWT/AES/Session/
|
|
1331
|
+
* Middleware/Encrypt…),且**不得以受保护动词开头** —— 否则
|
|
1332
|
+
* `Auth.changePassword` / `Auth.invalidateToken` / `Auth.add_user` 会被压掉。
|
|
1333
|
+
*/
|
|
1334
|
+
const AUTH_CLASS_EXACT = /^(?:Auth|Authorization|AccessControl|Permissions?)$/;
|
|
1335
|
+
const AUTH_MECHANISM_METHOD = /(?:Cache|Token|JWT|AES|Bearer|Session|Middleware|Encrypt|Decrypt|Payload|Signature)/i;
|
|
1336
|
+
const PROTECTED_VERB_PREFIX = /^(?:change|add|remove|delete|update|create|reset|invalidate|revoke|grant|deny|rotate)/i;
|
|
1337
|
+
function isAuthDecisionName(enclosingFuncName) {
|
|
1338
|
+
const dot = enclosingFuncName.lastIndexOf(".");
|
|
1339
|
+
if (dot <= 0)
|
|
1340
|
+
return false;
|
|
1341
|
+
if (!AUTH_CLASS_EXACT.test(enclosingFuncName.slice(0, dot)))
|
|
1342
|
+
return false;
|
|
1343
|
+
const method = enclosingFuncName.slice(dot + 1);
|
|
1344
|
+
if (PROTECTED_VERB_PREFIX.test(method))
|
|
1345
|
+
return false;
|
|
1346
|
+
return /^allow_/i.test(method) || AUTH_MECHANISM_METHOD.test(method);
|
|
1347
|
+
}
|
|
1348
|
+
/** 用户鉴权入口词表(login/register/logout/…)—— 原在两处重复定义,提成一份 */
|
|
1349
|
+
const AUTH_PATTERN = /\b(register|signup|signin|login|authenticate|createuser|createaccount|registeruser|registernewuser|dologin|verifytoken|validatesession|getuser|getsessionuser|getcurrentuser|endsession|logout|signout|dologout|destroysession|invalidatesession|invalidate|signout|create_account|register_new_user|register_user|sign_up|create_user|do_login|sign_in|log_in|verify_token|validate_session|get_user|get_session_user|get_current_user|do_logout|sign_out|log_out|end_session|invalidate_session|clear_session)\b/i;
|
|
1350
|
+
/**
|
|
1351
|
+
* 该函数是否是「鉴权相关」函数 —— 是则跳过 authorization 类目规则
|
|
1352
|
+
* (对鉴权机制自己报「未鉴权」是自指谬误)。
|
|
1353
|
+
*/
|
|
1354
|
+
function isAuthFunctionName(enclosingFuncName) {
|
|
1355
|
+
if (!enclosingFuncName)
|
|
1356
|
+
return false;
|
|
1357
|
+
const rawLower = enclosingFuncName.toLowerCase();
|
|
1358
|
+
if (AUTH_PATTERN.test(rawLower))
|
|
1359
|
+
return true;
|
|
1360
|
+
if (identifierParse(enclosingFuncName).some((w) => AUTH_PATTERN.test(w)))
|
|
1361
|
+
return true;
|
|
1362
|
+
if (isAuthMachineryName(enclosingFuncName))
|
|
1363
|
+
return true;
|
|
1364
|
+
if (isAuthDecisionName(enclosingFuncName))
|
|
1365
|
+
return true; // §40 C4
|
|
1366
|
+
// §40 C2:**函数自身**就是那个授权检查 ⇒ 对它报"缺检查"同样自指。
|
|
1367
|
+
// 例:verdaccio `canRemove` 报 "Authorization (Ownership Check)"。
|
|
1368
|
+
// 判据复用 §38 的三条(同源,不另起一份),只作用在**点号后的方法部分**。
|
|
1369
|
+
const dot = enclosingFuncName.lastIndexOf(".");
|
|
1370
|
+
const method = dot >= 0 ? enclosingFuncName.slice(dot + 1) : enclosingFuncName;
|
|
1371
|
+
if (exports.AUTHZ_PREDICATE_RE.test(method) ||
|
|
1372
|
+
exports.AUTHZ_CAN_HELPER_RE.test(method) ||
|
|
1373
|
+
exports.AUTHZ_ACCESS_CHECK_RE.test(method))
|
|
1374
|
+
return true;
|
|
1375
|
+
return false;
|
|
1376
|
+
}
|
|
1115
1377
|
/**
|
|
1116
1378
|
* Detect missing safeguards in function call sequences.
|
|
1117
1379
|
* Uses identifier parsing to match compound names (registerNewUser → register).
|
|
1118
1380
|
*/
|
|
1119
|
-
|
|
1381
|
+
/**
|
|
1382
|
+
* §44(2026-09-26)`paramGated` 的身份词表 —— 与旧的内联正则同源,抽出来是为了让
|
|
1383
|
+
* fixture 能 import 落地正则(R59),也让「主体 vs 被操作对象」这一刀有单一定义。
|
|
1384
|
+
*
|
|
1385
|
+
* ⚠ 这个词表**只能表达"参数名像身份"**,不能单独区分主体与被操作对象
|
|
1386
|
+
* (`token` 既可能是调用者凭证,也可能是被操作的 PAT)。
|
|
1387
|
+
* 区分靠**参数类型**(见 `classifyParamType`):主体是领域对象,被操作对象是标量。
|
|
1388
|
+
*/
|
|
1389
|
+
exports.IDENTITY_PARAM_RE = /\b(token|session|user|auth|request|scope|cookie|credential|permission|role|identity)\b/i;
|
|
1390
|
+
/** 标量类型 —— 被操作对象几乎总是这些(id / email / token 都是 string)。 */
|
|
1391
|
+
const SCALAR_PARAM_TYPE_RE = /^(?:string|number|boolean|bigint|symbol|void|never|date)(?:\[\])?$/i;
|
|
1392
|
+
/** 类型解析不出来 —— 保守,不收紧(ts-morph 对 `any` / 解不出类型的参数会给出这些)。 */
|
|
1393
|
+
const UNKNOWN_PARAM_TYPE_RE = /^(?:any|unknown|object)(?:\[\])?$/i;
|
|
1394
|
+
const NULLISH_PARAM_TYPE_RE = /^(?:null|undefined)$/i;
|
|
1395
|
+
/**
|
|
1396
|
+
* 参数类型三分类。**"unknown" 一律按旧行为放行** —— 类型通道不可用不是压制的理由。
|
|
1397
|
+
* 联合类型(`User | null` / `AuthenticationType | null`)按"含任一领域类型即 typed"处理。
|
|
1398
|
+
*/
|
|
1399
|
+
function classifyParamType(t) {
|
|
1400
|
+
const parts = (t || "")
|
|
1401
|
+
.split("|")
|
|
1402
|
+
.map((s) => s.trim())
|
|
1403
|
+
.filter(Boolean)
|
|
1404
|
+
.filter((s) => !NULLISH_PARAM_TYPE_RE.test(s));
|
|
1405
|
+
if (parts.length === 0)
|
|
1406
|
+
return "unknown";
|
|
1407
|
+
if (parts.every((s) => UNKNOWN_PARAM_TYPE_RE.test(s)))
|
|
1408
|
+
return "unknown";
|
|
1409
|
+
if (parts.some((s) => UNKNOWN_PARAM_TYPE_RE.test(s)))
|
|
1410
|
+
return "typed"; // 含 any 的联合 ⇒ 不敢判标量
|
|
1411
|
+
if (parts.every((s) => SCALAR_PARAM_TYPE_RE.test(s)))
|
|
1412
|
+
return "scalar";
|
|
1413
|
+
return "typed";
|
|
1414
|
+
}
|
|
1415
|
+
function detectSafeguardViolations(calls, enclosingFuncName, language, params, exposed, paramTypes) {
|
|
1120
1416
|
const violations = [];
|
|
1121
1417
|
// Build effective calls: raw names + identifier-parsed words.
|
|
1122
1418
|
// Class-qualified names (Class.method) contribute only their METHOD name —
|
|
@@ -1130,10 +1426,8 @@ function detectSafeguardViolations(calls, enclosingFuncName, language, params, e
|
|
|
1130
1426
|
}
|
|
1131
1427
|
const effectiveCalls = [...new Set([...rawCalls, ...parsedWords])];
|
|
1132
1428
|
// Skip authorization rules for auth functions — check both raw lowercased name and parsed words
|
|
1133
|
-
|
|
1134
|
-
const
|
|
1135
|
-
const isAuthFunction = enclosingFuncName != null && (AUTH_PATTERN.test(rawLower) ||
|
|
1136
|
-
identifierParse(enclosingFuncName).some(w => AUTH_PATTERN.test(w)));
|
|
1429
|
+
// §39:入口词表 ∪ 鉴权机制自身(自指排除),详见 isAuthFunctionName
|
|
1430
|
+
const isAuthFunction = isAuthFunctionName(enclosingFuncName);
|
|
1137
1431
|
// Filter rules by language
|
|
1138
1432
|
const activeRules = language
|
|
1139
1433
|
? SAFEGUARD_RULES.filter(r => !r.languages || r.languages.includes(language))
|
|
@@ -1148,9 +1442,16 @@ function detectSafeguardViolations(calls, enclosingFuncName, language, params, e
|
|
|
1148
1442
|
const triggerMatch = triggerCalls.some(c => rule.trigger.test(c));
|
|
1149
1443
|
if (!triggerMatch)
|
|
1150
1444
|
continue;
|
|
1151
|
-
// Semantic precondition marker (extractor-emitted)
|
|
1152
|
-
|
|
1153
|
-
|
|
1445
|
+
// Semantic precondition marker (extractor-emitted).
|
|
1446
|
+
// E5(2026-09-23):只在能产出该标记的语言上强制。不限定语言的话,
|
|
1447
|
+
// Python 侧(提取器不产出任何 __progmune_* 标记)会静默归零——
|
|
1448
|
+
// 而 TS 侧所有闸门都看不见这种退化(R42)。
|
|
1449
|
+
if (rule.requireMarker) {
|
|
1450
|
+
const enforce = !rule.requireMarkerLanguages
|
|
1451
|
+
|| (language ? rule.requireMarkerLanguages.includes(language) : true);
|
|
1452
|
+
if (enforce && !effectiveCalls.includes(rule.requireMarker))
|
|
1453
|
+
continue;
|
|
1454
|
+
}
|
|
1154
1455
|
// Skip authorization rules for auth functions — they ARE the auth
|
|
1155
1456
|
if (isAuthFunction && rule.category === "authorization")
|
|
1156
1457
|
continue;
|
|
@@ -1164,8 +1465,23 @@ function detectSafeguardViolations(calls, enclosingFuncName, language, params, e
|
|
|
1164
1465
|
// Surface gate (paramGated): only apply to functions that can plausibly
|
|
1165
1466
|
// authenticate — routed by a web handler (exposed) or taking an
|
|
1166
1467
|
// identity-ish parameter. Requires the caller to pass param names.
|
|
1468
|
+
//
|
|
1469
|
+
// §44(2026-09-26):光看参数名会把**被操作对象**当成**调用者身份** ——
|
|
1470
|
+
// `updateLastUsedForPAT(token: string)` 的 token 是要被更新的 PAT;
|
|
1471
|
+
// `addUserToTeam(teamID, userEmail, role)` 的 role 是要授予的角色。
|
|
1472
|
+
// 主体是**领域对象**(`user: AuthUser` / `user: User` / `adminUser: Admin`),
|
|
1473
|
+
// 被操作对象几乎总是**标量**(id / email / token 都是 string)⇒ 标量类型的参数
|
|
1474
|
+
// 不再充当身份证据。类型通道不可用时不收紧(退回旧行为,见 R57)。
|
|
1167
1475
|
if (rule.paramGated && params) {
|
|
1168
|
-
const hasIdentity = params.some(p =>
|
|
1476
|
+
const hasIdentity = params.some((p, i) => {
|
|
1477
|
+
if (!exports.IDENTITY_PARAM_RE.test(p))
|
|
1478
|
+
return false;
|
|
1479
|
+
const t = paramTypes?.[i];
|
|
1480
|
+
// 调用方没传类型 / 该参数类型解析不出来(any、unknown、空)⇒ 保守放行
|
|
1481
|
+
if (t === undefined)
|
|
1482
|
+
return true;
|
|
1483
|
+
return classifyParamType(t) !== "scalar";
|
|
1484
|
+
});
|
|
1169
1485
|
if (!hasIdentity && !exposed)
|
|
1170
1486
|
continue;
|
|
1171
1487
|
}
|
|
@@ -1278,10 +1594,8 @@ function detectSafeguardViolationsV7(calls, enclosingFuncName, callerMap, funcCa
|
|
|
1278
1594
|
for (const c of triggerEffectiveCalls)
|
|
1279
1595
|
safeContext.add(c);
|
|
1280
1596
|
// Skip authorization rules for auth functions
|
|
1281
|
-
|
|
1282
|
-
const
|
|
1283
|
-
const isAuthFunction = enclosingFuncName != null && (AUTH_PATTERN.test(rawLower) ||
|
|
1284
|
-
identifierParse(enclosingFuncName).some(w => AUTH_PATTERN.test(w)));
|
|
1597
|
+
// §39:入口词表 ∪ 鉴权机制自身(自指排除),详见 isAuthFunctionName
|
|
1598
|
+
const isAuthFunction = isAuthFunctionName(enclosingFuncName);
|
|
1285
1599
|
// Filter rules by language
|
|
1286
1600
|
const activeRulesV7 = language
|
|
1287
1601
|
? SAFEGUARD_RULES.filter(r => !r.languages || r.languages.includes(language))
|
package/dist/sdk.js
CHANGED
|
@@ -20,7 +20,7 @@ const risk_model_1 = require("./risk-model");
|
|
|
20
20
|
const protocol_knowledge_1 = require("./protocol-knowledge");
|
|
21
21
|
const evidence_repository_1 = require("./evidence-repository");
|
|
22
22
|
/** Runtime version — stable public identifier. Internal layers evolve underneath. */
|
|
23
|
-
exports.RUNTIME_VERSION = "3.7.
|
|
23
|
+
exports.RUNTIME_VERSION = "3.7.52";
|
|
24
24
|
function verify(filePath) {
|
|
25
25
|
const cert = (0, certify_1.certify)(filePath);
|
|
26
26
|
const kb = (0, protocol_knowledge_1.buildKnowledgeBase)();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "progmune-runtime",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.53",
|
|
4
4
|
"description": "Progmune — AI Trust Decision Engine. Verify AI-generated code before it reaches production. Outputs APPROVED / NEEDS_REVIEW / BLOCKED with evidence.",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist/",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
25
|
"prepare": "npm run build",
|
|
26
|
-
"build": "tsc -p tsconfig.json && tsc -p tsconfig.mcp.json && mv dist-mcp/mcp-server.js dist/mcp-server.mjs && rm -rf dist-mcp",
|
|
26
|
+
"build": "tsc -p tsconfig.json && tsc -p tsconfig.mcp.json && mv dist-mcp/mcp-server.js dist/mcp-server.mjs && (rm -rf dist-mcp || true)",
|
|
27
27
|
"start": "node dist/mcp-server.mjs",
|
|
28
28
|
"test": "node dist/check.js",
|
|
29
29
|
"test:unit": "vitest run --exclude '**/stress/**' --exclude '**/soak/**' --exclude '**/chaos/**'",
|