progmune-runtime 3.7.52 → 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 CHANGED
@@ -1,5 +1,43 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.7.53] — 2026-09-26
4
+
5
+ ### 授权族误报压降收口 + 三处可见性基建(§35-§46 合并发布)
6
+
7
+ **可见性基建(§35-§37,只读探针,不进产品路径)**:跨函数调用链视图原型;采样单位从
8
+ 「文件」换成「入口的调用闭包」;入口根可达性视图(「谁会走到我」)。三者共同解决
9
+ **切片看不出跨函数证据**的问题——后续每一轮压降都靠它们回答「这一刀在哪个语料上被触发」。
10
+
11
+ **两处判据进主干(§38、§39)**:
12
+ - 对象级授权判据从「动词白名单」换成「组合式谓语」,不再靠 `get/list/create` 这类词根猜语义
13
+ - D 类自指排除:把**鉴权机制自身**也识别为 auth function(此前机制代码被当成未鉴权的业务代码)
14
+
15
+ **授权族压降(§40-§46)**:
16
+ - §42 补 `webshape_G`(17 条授权形态)——**此前盲测在授权命名轴上根本没有差分能力**,
17
+ 改任何判据都看不出效果;补形后项目 117→118、Authorization 族 938→947
18
+ - §44 `paramGated`:**标量参数不是调用者身份**。函数参数名出现 `userId/ownerId` 但类型是
19
+ 裸 string/number 的,不再当鉴权证据(`classifyParamType` 三分类);真实池 337→333、
20
+ 盲测 2887→2618,269 条损失全为 dispatcher 重复投影
21
+ - §45 修同族 safeguard 词表漂移(Unauthenticated Access 缺 `getSession/validateSession/
22
+ verifyToken`)——漂移的表现是**同样形状的一条报、一条不报**;盲测 2618→2610
23
+ - §46 ① `credential_issuance`:`create_*_token` 语义是**签发**不是**校验**,从 `auth_check`
24
+ 移出单列(行为不变,消除误改诱因);② `owner_self_assigned`:只认**绑定形态**
25
+ `{ownerId: user.id}`,不认实参形态 `f(x, user.id)`(真实池 3/3 FP vs 合成 1/1 TP,
26
+ 真值相反 ⇒ R71),同刻比对压掉 197 条全 FP、真漏洞零伤
27
+
28
+ **三条被数据否决的(不落地)**:§43「容器后缀=Service」(造不出反例,R66)、
29
+ §45「triggerOwnNameOnly」(R66 二次命中)、§46③「safeguard 只看调用不看是否检查返回结果」
30
+ (新增 26 条全是已知 dispatcher 误报,收益 0,R72)。
31
+
32
+ **方法学累计**:R66-R73。其中两条是**基线陷阱**——R67(冻结母本不是同刻基线,误报压制数
33
+ 48→真实 4)、R73(上一次的 results 也不是同刻基线,误报压制数 12→真实 197,低估 16 倍)。
34
+ 正确口径只有一个:**摘掉这一刀、其余不动、重跑**。次要:R68(dist 不是 src 的同步副本,
35
+ 陈旧是静默的)、R69(同族词表要定期做集合 diff)、R70(label 必须与语义一致)。
36
+
37
+ **门**:tsc 0 错 / webshape 66-0 / taintpath 160-0 / fr-007 pre5·post0 + fr-016 pre7·post0 /
38
+ 定向 9 文件 329 条全绿 / 反向验证:② 摘刀 197 条全部回来、① 摘刀 fixture 转红、
39
+ REVERSE-VERIFY 残留 0 / 真实切片 LOST 0·ADDED 0
40
+
3
41
  ## [3.7.52] — 2026-09-23
4
42
 
5
43
  ### 语料补形 + 两处基础设施修复 + E4/E5 两个确定性通道(§26-§31 合并发布)
@@ -379,6 +379,19 @@ function extractDirectCalls(func, preText) {
379
379
  if (/ownerId\s*[!=]==?|authorId\s*[!=]==?|createdBy\s*[!=]==?|\.owner\s*[!=]==?|userId\s*[!=]==?/.test(text)) {
380
380
  calls.push("__progmune_ownership_checked__");
381
381
  }
382
+ // §46 ②:新建资源的属主被**赋值为当前主体**(`{ userId: user.id }` / `ownerId: currentUser.id`)。
383
+ // 这类「创建」函数的被操作对象就是主体自己,不存在「改别人的东西」这回事 ⇒
384
+ // Ownership Check 不该报(合成语料上 10/10 是 FP:createTransfer / createRefund /
385
+ // uploadFile / placeOrder…)。调用名接口看不到对象字面量里的赋值,只能靠标记。
386
+ //
387
+ // ⚠ 只认 `<属主字段>: <主体>.id` 这一种形态,**不认** `<调用>(..., user.id, ...)`:
388
+ // 后者是「主体标识作实参」,实测同形状存在相反真值 ——
389
+ // `deletePAT(id, user.uid)`(已按属主过滤,FP)vs `deleteWidget(id)` 里
390
+ // `canSendEmail(user.id)`(user.id 与 deleteWidget 无关,真 TP)⇒ 按 R66 不得落地。
391
+ // 形态:`<属主字段>: <主体变量>.<标识属性>` (对象字面量内的赋值,不是查询比较)
392
+ if (/\b(?:userId|ownerId|authorId|createdBy|user_id|owner_id|author_id|created_by)\s*:\s*(?:user|currentUser|authUser|me|self|actor|principal|adminUser|(?:request|req|session|ctx|context)\.user)\s*\.\s*(?:id|uid|_id|userId|email|username)\b/i.test(text)) {
393
+ calls.push("__progmune_owner_self_assigned__");
394
+ }
382
395
  return [...new Set(calls)];
383
396
  }
384
397
  // ═══════════════════════════════════════════════════════════════
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * §41 E1(2026-09-25):token 内嵌 acronym 的**局部**补认 —— 正/反向 fixture
5
+ *
6
+ * 背景:C5 原案是「改主干 identifierParse 让 JWTPayload 拆出 JWT」。
7
+ * `split-probe.ts` 实测否决了它(详见 src 里 ACRONYM_PREFIX 的注释):
8
+ * 803 个去重标识符里只有 16 个受影响、新增 22 词中仅 2 个(JWT/OIDC)是机制类,
9
+ * 其余是 `OAuthToken→Auth`、`createATeam→A,Team` 这类噪声;
10
+ * 而 identifierParse 还供给 triggerParsedWords ⇒ 动它等于改全局触发面。
11
+ * ⇒ 最终落地的是**只在 isAuthMachineryName 的对象侧**补认 acronym。
12
+ *
13
+ * 为什么必须有这里的 fixture(同 §40,R56):改动方向是「压掉告警」,
14
+ * 而 generated/ 合成语料里**没有** `verifyJWTPayload` 这类形态 ⇒
15
+ * 盲测零漂移很可能是零触发。**这里是唯一能证明这段判据活着的证据。**
16
+ */
17
+ const vitest_1 = require("vitest");
18
+ const protocol_detector_1 = require("./protocol-detector");
19
+ /** 只关心 authorization 类目(本轮只影响这一类目) */
20
+ const authz = (fnName, calls = ["deleteSpace", "findOne"]) => (0, protocol_detector_1.detectSafeguardViolations)(calls, fnName, "typescript", ["user", "id"], true).filter((v) => /^Authorization/i.test(v.rule));
21
+ (0, vitest_1.describe)("§41 E1 正向:acronym 卡在函数名的机制对象里(应该被认成鉴权机制)", () => {
22
+ (0, vitest_1.it)("verifyJWTPayload —— §40 剩余池待救的那条(verdaccio packages/auth/src/utils.ts)", () => {
23
+ (0, vitest_1.expect)(authz("verifyJWTPayload")).toHaveLength(0);
24
+ });
25
+ (0, vitest_1.it)("verifyJWTPayload 带触发 trigger 也一样压", () => {
26
+ (0, vitest_1.expect)(authz("verifyJWTPayload", ["getTarball", "readPackage"])).toHaveLength(0);
27
+ });
28
+ (0, vitest_1.it)("JWTRemote 类托管的 jwt 机制方法", () => {
29
+ (0, vitest_1.expect)(authz("JwtRemote.getJWTRemote")).toHaveLength(0);
30
+ });
31
+ (0, vitest_1.it)("类限定形式 AvailabilityService.verifyJWTPayload", () => {
32
+ (0, vitest_1.expect)(authz("AvailabilityService.verifyJWTPayload")).toHaveLength(0);
33
+ });
34
+ (0, vitest_1.it)("createJWTToken —— 唯有 E1 才救得回来的一条(反向验证揪出来的错误假设)", () => {
35
+ // ⚠ 这条曾经被我标成「BASE 就命中」,是**错的**:
36
+ // identifierParse("createJWTToken") → ["create","JWTToken"]
37
+ // ⇒ 对象侧只拿到 "jwttoken",既 ≠ jwt 也 ≠ token ⇒ BASE **不命中**。
38
+ // 反向验证(把 machineryWords 退回 identifierParse)让它转红才发现。
39
+ // 教训同 R59:凭印象断言既有行为,与口径漂移是同一类错误。
40
+ (0, vitest_1.expect)(authz("createJWTToken")).toHaveLength(0);
41
+ });
42
+ (0, vitest_1.it)("⚠ verifyOIDCToken 不收 —— oidc 只在**容器侧**名词表,**不在方法对象词表**", () => {
43
+ // §39.8 的收窄是有意的:对象侧一旦放宽到 oauth/oidc/saml,
44
+ // `ApiTokensController.deleteToken` 这类凭据 CRUD 会被误压(V-D 教训)。
45
+ // 剩余池里没有待救的 verifyOIDCToken 形态 ⇒ 按 R50 不为此扩词表。
46
+ (0, vitest_1.expect)(authz("verifyOIDCToken").length).toBeGreaterThan(0);
47
+ });
48
+ (0, vitest_1.it)("verifySSLSignature 不在机制对象词表 ⇒ 仍报(词表不含 ssl,别幻想)", () => {
49
+ (0, vitest_1.expect)(authz("verifySSLSignature").length).toBeGreaterThan(0);
50
+ });
51
+ });
52
+ /**
53
+ * `tokenAcronyms` 的直接单元测试 —— 端到端 fixture 覆盖不到这两条约束,
54
+ * 因为**对象侧 acronym 与 BASE 的重叠度极高**(凡 acronym 等于 jwt/token/…
55
+ * 的,BASE 下独立 token 本来也就命中了),端到端分不出「约束生效」与
56
+ * 「恰好 BASE 也命中」。剩下来真正会被 `${2,}` 拦住的,只有单字母情形。
57
+ */
58
+ (0, vitest_1.describe)("§41 tokenAcronyms 表驱动:两条约束(≥2 字母 / 贪婪回溯)", () => {
59
+ const cases = [
60
+ ["JWTPayload", ["JWT"]],
61
+ ["OIDCStrategy", ["OIDC"]], // ← 贪婪:非贪婪会停在 "OID",救不了这条
62
+ ["HTTPErr", ["HTTP"]],
63
+ ["CLIError", ["CLI"]],
64
+ ["JWTToken", ["JWT"]],
65
+ // 以下必须为空:单字母 / 纯 PascalCase / 已有独立词
66
+ ["OAuth", []], // ← 放宽到 {1,} 就会产出 "O"
67
+ ["ATeam", []],
68
+ ["YElement", []],
69
+ ["Token", []],
70
+ ["Payload", []],
71
+ ["payload", []],
72
+ ["JWTPayloadService", ["JWT"]], // 边界:最后一个大写归后词 ⇒ 不会吃成 "JWTP"
73
+ ];
74
+ for (const [input, want] of cases) {
75
+ (0, vitest_1.it)(`${input} → ${JSON.stringify(want)}`, () => {
76
+ (0, vitest_1.expect)((0, protocol_detector_1.tokenAcronyms)(input)).toEqual(want);
77
+ });
78
+ }
79
+ });
80
+ (0, vitest_1.describe)("§41 E1 反向:不得把非机制名字误认成机制(R57 宽变体约束)", () => {
81
+ (0, vitest_1.it)("⚠ validateOAuthToken —— BASE 就会命中(validate × Token),与 acronym 无关", () => {
82
+ // 存在意义:端到端根本测不出「OAuth 被拆出 Auth」—— 因为这条 BASE 下也压。
83
+ // 若有人把 `{2,}` 放宽成 `{1,}`,`OAuth` → "O",而 "O" 不在任何词表里
84
+ // ⇒ 端到端行为完全不变,**回归无从发现**。单字母噪声只有在表驱动层才可见。
85
+ (0, vitest_1.expect)(authz("validateOAuthToken")).toHaveLength(0);
86
+ });
87
+ (0, vitest_1.it)("⚠ createATeam 不得拆出 Team 之外的机制词", () => {
88
+ (0, vitest_1.expect)(authz("AdminService.createATeam").length).toBeGreaterThan(0);
89
+ });
90
+ (0, vitest_1.it)("⚠ throwHTTPErr 不得因 HTTP 被当成机制", () => {
91
+ (0, vitest_1.expect)(authz("throwHTTPErr").length).toBeGreaterThan(0);
92
+ });
93
+ (0, vitest_1.it)("⚠ createCLIErrorResponse 不得因 CLI/Error 被当成机制", () => {
94
+ (0, vitest_1.expect)(authz("createCLIErrorResponse").length).toBeGreaterThan(0);
95
+ });
96
+ (0, vitest_1.it)("⚠ 纯 PascalCase 词不得被再切一遍(Token → T/oken 之类的臆想不生效)", () => {
97
+ (0, vitest_1.expect)(authz("deleteExpiredToken").length).toBeGreaterThan(0);
98
+ });
99
+ (0, vitest_1.it)("⚠ 动词缺失 ⇒ acronym 对象不算(只有对象没有机制动词)", () => {
100
+ // mw 必须同时含 VERB × OBJECT;JWTPayload 单独不是机制
101
+ (0, vitest_1.expect)(authz("logJWTPayload").length).toBeGreaterThan(0);
102
+ });
103
+ /**
104
+ * 最要紧的一条反向:**凭据增删改**即使名字里含 JWT 也不能压 —— 与 §39 V-D、
105
+ * §40 W4 同源教训。这条守的是「约束在于组合,不在于 acronym 本身」。
106
+ */
107
+ (0, vitest_1.it)("⚠️ createJWTToken 是依赖 E1 的哨兵 ⇒ 反向验证时必须转红", () => {
108
+ // §41 反向验证实测(把 machineryWords 退回 identifierParse):
109
+ // 全套 76 条里 **6 条**转红,其余 70 条(§38/§39/§40)纹丝不动
110
+ // ⇒ E1 的影响面被严格锁在「机制对象侧 acronym」这一格,没有横向污染。
111
+ // 这条 earlier 被错写成「GENERATE 类动词…既有口径一致」—— 与 R59 同源的
112
+ // 「凭印象断言既有行为」,已据反向验证的实测更正。
113
+ (0, vitest_1.expect)(authz("createJWTToken")).toHaveLength(0);
114
+ });
115
+ (0, vitest_1.it)("⚠ 与 §40 C4 的分工:容器=Auth 且方法受保护动词开头 ⇒ 不压", () => {
116
+ (0, vitest_1.expect)(authz("Auth.changeJWTToken").length).toBeGreaterThan(0);
117
+ });
118
+ });
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * §45:Authorization (Unauthenticated Access) 的 auth_check 词表漂移修复。
5
+ *
6
+ * 病灶:同一族的 Unauthenticated Mutation 认 `getSession` / `validateSession` / `verifyToken`,
7
+ * Unauthenticated Access 只认 `getSessionUser`,**不认裸 `getSession`**。
8
+ * 于是「先取会话再操作」的读类函数被判成「未认证即可读」—— 真实案例 filestorage 的
9
+ * uploadFile / deleteFile / createFolder / createShareLink 四条(体里都有
10
+ * `const user = getSession(token); if (!user) return null;`)。
11
+ *
12
+ * 修复:把三个词补进 Access 规则的 auth_check(与 Mutation 对齐)。
13
+ * 不做:三个 mint 类词(create_access_token / create_jwt_token / create_refresh_token)——
14
+ * 「签发令牌」不是「校验调用者」,语义不同,不在本轮(见设计文档 §45.6)。
15
+ */
16
+ const vitest_1 = require("vitest");
17
+ const protocol_detector_1 = require("./protocol-detector");
18
+ const ACC = "Authorization (Unauthenticated Access)";
19
+ const MUT = "Authorization (Unauthenticated Mutation)";
20
+ /** 单 LANGUAGE=typescript;paramGate 用 user:User 放行(类型是非标量领域类型) */
21
+ function dsv(calls, name) {
22
+ return (0, protocol_detector_1.detectSafeguardViolations)(calls, name, "typescript", ["user"], false, ["User"]);
23
+ }
24
+ const has = (calls, name, rule) => dsv(calls, name).some((v) => v.rule === rule);
25
+ (0, vitest_1.describe)("§45 getSession 漂移:读类函数已取会话 ⇒ 不再报 Unauthenticated Access", () => {
26
+ (0, vitest_1.it)("getSession 应被认作认证检查(filestorage uploadFile 原型)", () => {
27
+ (0, vitest_1.expect)(has(["getSession", "join", "writeFileSync"], "uploadFile", ACC)).toBe(false);
28
+ });
29
+ (0, vitest_1.it)("validateSession 应被认作认证检查", () => {
30
+ (0, vitest_1.expect)(has(["validateSession", "find"], "listDocuments", ACC)).toBe(false);
31
+ });
32
+ (0, vitest_1.it)("verifyToken 应被认作认证检查", () => {
33
+ (0, vitest_1.expect)(has(["verifyToken", "find"], "getDocument", ACC)).toBe(false);
34
+ });
35
+ (0, vitest_1.it)("类限定调用形态 this.auth.getSession() 也成立(identifierParse 拆词后命中)", () => {
36
+ (0, vitest_1.expect)(has(["this", "auth", "getSession", "find"], "deleteFile", ACC)).toBe(false);
37
+ });
38
+ });
39
+ (0, vitest_1.describe)("§45 反向对照:真正的未认证读取必须仍然报", () => {
40
+ (0, vitest_1.it)("getDocuments 无任何认证调用 ⇒ 仍报", () => {
41
+ (0, vitest_1.expect)(has(["find", "filter"], "getDocuments", ACC)).toBe(true);
42
+ });
43
+ (0, vitest_1.it)("listReports 无任何认证调用 ⇒ 仍报", () => {
44
+ (0, vitest_1.expect)(has(["filter", "map"], "listReports", ACC)).toBe(true);
45
+ });
46
+ (0, vitest_1.it)("downloadExport 无任何认证调用 ⇒ 仍报", () => {
47
+ (0, vitest_1.expect)(has(["readFileSync", "pipe"], "downloadExport", ACC)).toBe(true);
48
+ });
49
+ (0, vitest_1.it)("getSessionUser 之外的无关 get* 不构成认证证据(getConfig 类)", () => {
50
+ // getConfig 走 excludePatterns;这里用 getUniqueId 确保普通取值不算检查
51
+ (0, vitest_1.expect)(has(["getUniqueId", "find"], "listAuditLogs", ACC)).toBe(true);
52
+ });
53
+ });
54
+ (0, vitest_1.describe)("§45 与 Mutation 规则的对称性:同一形态两条规则都不报", () => {
55
+ const calls = ["getSession", "find"];
56
+ (0, vitest_1.it)("读侧", () => (0, vitest_1.expect)(has(calls, "listInvoices", ACC)).toBe(false));
57
+ (0, vitest_1.it)("写侧", () => (0, vitest_1.expect)(has(["getSession", "push"], "createInvoice", MUT)).toBe(false));
58
+ });
59
+ (0, vitest_1.describe)("§45 已知未覆盖范围(登记为债,不是 bug)", () => {
60
+ (0, vitest_1.it)("⚠ 只看「是否调用」getSession,不看「是否检查其结果」—— 这是既有宽松", () => {
61
+ // `const u = getSession(t); return data;` (不检查 u)仍会被判为已认证。
62
+ // 单函数视角拿不到数据流,本轮不修;见设计文档 §45.6。
63
+ (0, vitest_1.expect)(has(["getSession", "return"], "getEverything", ACC)).toBe(false);
64
+ });
65
+ });
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * §39(2026-09-25)D 类自指排除 —— 正/反向 fixture
5
+ *
6
+ * 为什么必须有这个文件:与 §38 同因。本轮改的是「哪些函数算鉴权机制自身」,
7
+ * 盲测语料(blind-benchmark/generated/ 合成项目)里没有 JwtAuthGuard /
8
+ * AbilityFactory 这类形态 ⇒ 盲测会零漂移,但同时**一条都没触发**(R56:
9
+ * 零漂移 ≠ 验证通过)。没有这里的 fixture,这段判据就是零覆盖进主干。
10
+ *
11
+ * ⚠ 反向 fixture 比正向更要紧:这条判据的作用是**压掉**告警,
12
+ * 压错方向就是真漏报且不可见(R49 的镜像)。所以每一条「不得压掉」的
13
+ * 形态都必须钉死,尤其是**对凭据做增删改**的端点(那是最需要鉴权的地方)。
14
+ *
15
+ * 每条都对应真值集/真实扫描里人工核对过的代码:
16
+ * JwtAuthGuard.handleRequest ← docmost src/common/guards/jwt-auth.guard.ts
17
+ * WorkspaceAbilityFactory.createForUser ← docmost src/core/casl/abilities/
18
+ * ApiTokensController.deleteToken ← hedgedoc(凭据 CRUD,不得压)
19
+ * AccessTokenService.updateLastUsedForPAT ← hoppscotch(凭据 CRUD,不得压)
20
+ */
21
+ const vitest_1 = require("vitest");
22
+ const protocol_detector_1 = require("./protocol-detector");
23
+ /** 只关心 authorization 类目(本轮只影响这一类目) */
24
+ const authz = (fnName) => (0, protocol_detector_1.detectSafeguardViolations)(["deleteSpace", "findOne"], fnName, "typescript", ["user", "id"], true).filter((v) => /^Authorization/i.test(v.rule));
25
+ (0, vitest_1.describe)("§39 自指排除 —— 正对照(鉴权机制自身 ⇒ 压掉)", () => {
26
+ (0, vitest_1.it)("JwtAuthGuard 守卫本体", () => {
27
+ (0, vitest_1.expect)(authz("JwtAuthGuard.handleRequest")).toHaveLength(0);
28
+ });
29
+ (0, vitest_1.it)("JwtAuthGuard 里写 cookie", () => {
30
+ (0, vitest_1.expect)(authz("JwtAuthGuard.setJoinedWorkspacesCookie")).toHaveLength(0);
31
+ });
32
+ (0, vitest_1.it)("Auth 控制器置 cookie(登录流程的一部分)", () => {
33
+ (0, vitest_1.expect)(authz("AuthController.setAuthCookie")).toHaveLength(0);
34
+ });
35
+ (0, vitest_1.it)("Auth 服务签发协作 token", () => {
36
+ (0, vitest_1.expect)(authz("AuthService.getCollabToken")).toHaveLength(0);
37
+ });
38
+ (0, vitest_1.it)("Token 服务校验 jwt(纯原语)", () => {
39
+ (0, vitest_1.expect)(authz("TokenService.verifyJwt")).toHaveLength(0);
40
+ });
41
+ (0, vitest_1.it)("CASL 授权引擎本体", () => {
42
+ (0, vitest_1.expect)(authz("WorkspaceAbilityFactory.createForUser")).toHaveLength(0);
43
+ });
44
+ (0, vitest_1.it)("签发 JWT", () => {
45
+ (0, vitest_1.expect)(authz("UserService.generateJWT")).toHaveLength(0);
46
+ });
47
+ (0, vitest_1.it)("从请求里取 token(裸函数名,无类限定)", () => {
48
+ (0, vitest_1.expect)(authz("getApiToken")).toHaveLength(0);
49
+ });
50
+ });
51
+ (0, vitest_1.describe)("§39 自指排除 —— 反对照(不是机制 ⇒ 不得压掉)", () => {
52
+ (0, vitest_1.it)("业务函数:删收藏(「user 作为实参」的 B 类,不是机制)", () => {
53
+ (0, vitest_1.expect)(authz("FavoriteController.removeFavorite").length).toBeGreaterThan(0);
54
+ });
55
+ (0, vitest_1.it)("业务函数:删附件图标", () => {
56
+ (0, vitest_1.expect)(authz("AttachmentController.removeIcon").length).toBeGreaterThan(0);
57
+ });
58
+ (0, vitest_1.it)("业务函数:删空间", () => {
59
+ (0, vitest_1.expect)(authz("SpaceController.deleteSpace").length).toBeGreaterThan(0);
60
+ });
61
+ (0, vitest_1.it)("⚠ 凭据 CRUD:删 token —— 最需要鉴权,绝不能压", () => {
62
+ (0, vitest_1.expect)(authz("ApiTokensController.deleteToken").length).toBeGreaterThan(0);
63
+ });
64
+ (0, vitest_1.it)("⚠ 凭据 CRUD:删 PAT —— 绝不能压", () => {
65
+ (0, vitest_1.expect)(authz("AccessTokenController.deletePAT").length).toBeGreaterThan(0);
66
+ });
67
+ (0, vitest_1.it)("⚠ 凭据 CRUD:改 PAT 使用时间 —— 绝不能压", () => {
68
+ (0, vitest_1.expect)(authz("AccessTokenService.updateLastUsedForPAT").length).toBeGreaterThan(0);
69
+ });
70
+ (0, vitest_1.it)("限流守卫不是鉴权机制(ThrottlerGuard)", () => {
71
+ (0, vitest_1.expect)(authz("ThrottlerGuard.handleRequest").length).toBeGreaterThan(0);
72
+ });
73
+ (0, vitest_1.it)("通用设计模式后缀不是鉴权机制(CacheStrategy)", () => {
74
+ // 只看 Strategy 后缀会误吃 —— 必须配凭据名词约束
75
+ (0, vitest_1.expect)(authz("CacheStrategy.get").length).toBeGreaterThan(0);
76
+ });
77
+ (0, vitest_1.it)("受保护操作:改密码不属于机制", () => {
78
+ (0, vitest_1.expect)(authz("AuthService.changePassword").length).toBeGreaterThan(0);
79
+ });
80
+ (0, vitest_1.it)("待定用户数据删除不属于机制", () => {
81
+ (0, vitest_1.expect)(authz("AuthController.deletePendingUserData").length).toBeGreaterThan(0);
82
+ });
83
+ });
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * §40(2026-09-25)剩余池三处判据缺口 —— 正/反向 fixture
5
+ *
6
+ * 与 §38/§39 同因:这三处都是**压掉告警**方向的改动,盲测语料(generated/ 合成项目)
7
+ * 里没有 `Auth.allow_*` / 裸 `can('publish')` / `canRemove` 这类形态 ⇒ 盲测即便
8
+ * 零漂移也可能**一条都没触发**(R56:零漂移 ≠ 验证通过)。没有这里的 fixture,
9
+ * 这三段判据就是零覆盖进主干。
10
+ *
11
+ * 三处缺口的来源(都是人工核剩余池 35 条时逐条读源码读出来的):
12
+ * C1 裸 can — verdaccio `publish()`:`const can = allow(auth, …)`,
13
+ * 路由上 `can('publish')`。calls 里是**裸** can,
14
+ * §38 的谓语要求 `can<动作词>` ⇒ 落空。
15
+ * C2 自身谓语名 — verdaccio `canRemove` 报 "Ownership Check":它**就是**那个检查。
16
+ * §39 的自指排除只认 AUTH_PATTERN + 机制名,不认 `can<X>`。
17
+ * C4 容器=Auth — §39.8 记下的未收项:`Auth.allow_*` / `Auth.setLegacyAuthCacheEntry`。
18
+ *
19
+ * ⚠⚠ 反向 fixture 比正向更要紧(R57):C4 的**宽变体**(容器放宽到 `Auth*`)实测会吃掉
20
+ * `AuthController.deletePendingUserData` / `Auth.changePassword` / `Auth.invalidateToken`
21
+ * —— 对凭据与待处理用户做增删改恰恰是最需要鉴权的地方。所以 C4 必须
22
+ * 「严格等于裸类名」×「方法是授权决策(allow_)或机制名词」两层约束,两条都要。
23
+ */
24
+ const vitest_1 = require("vitest");
25
+ const protocol_detector_1 = require("./protocol-detector");
26
+ /** 只关心 authorization 类目(本轮只影响这一类目) */
27
+ const authz = (fnName, calls = ["deleteSpace", "findOne"]) => (0, protocol_detector_1.detectSafeguardViolations)(calls, fnName, "typescript", ["user", "id"], true).filter((v) => /^Authorization/i.test(v.rule));
28
+ (0, vitest_1.describe)("§40 C1 裸 can(CASL ability.can / verdaccio can('publish'))", () => {
29
+ (0, vitest_1.it)("函数体里调用裸 can ⇒ 认为做了授权判定(verdaccio publish 同形)", () => {
30
+ // publish 的 trigger 来自 publishPackage / removePackage 等,这里用等价形态
31
+ (0, vitest_1.expect)(authz("publish", ["can", "put", "publishPackage", "removePackage"])).toHaveLength(0);
32
+ });
33
+ (0, vitest_1.it)("裸 cannot 同形", () => {
34
+ (0, vitest_1.expect)(authz("publish", ["cannot", "put", "publishPackage"])).toHaveLength(0);
35
+ });
36
+ (0, vitest_1.it)("限定形式 ability.can(CASL 标准用法)", () => {
37
+ (0, vitest_1.expect)(authz("publish", ["ability.can", "publishPackage"])).toHaveLength(0);
38
+ });
39
+ (0, vitest_1.it)("⚠ cancel 不是 can —— 精确匹配,不得因同形被收", () => {
40
+ (0, vitest_1.expect)(authz("publish", ["cancel", "publishPackage"]).length).toBeGreaterThan(0);
41
+ });
42
+ (0, vitest_1.it)("⚠ candidate / canary 也不是 —— 精确匹配钉死", () => {
43
+ (0, vitest_1.expect)(authz("publish", ["candidate", "canary", "publishPackage"]).length).toBeGreaterThan(0);
44
+ });
45
+ });
46
+ (0, vitest_1.describe)("§40 C2 函数名自身就是授权谓语(自指)", () => {
47
+ (0, vitest_1.it)("canRemove 报「缺所有权检查」是自指谬误", () => {
48
+ (0, vitest_1.expect)(authz("canRemove")).toHaveLength(0);
49
+ });
50
+ (0, vitest_1.it)("canPublish 同理", () => {
51
+ (0, vitest_1.expect)(authz("canPublish")).toHaveLength(0);
52
+ });
53
+ (0, vitest_1.it)("cannotDelete 同理(cannot 分支)", () => {
54
+ (0, vitest_1.expect)(authz("cannotDelete")).toHaveLength(0);
55
+ });
56
+ (0, vitest_1.it)("类限定的 Auth.canRemove 同理", () => {
57
+ (0, vitest_1.expect)(authz("StorageViewCommand.canRemove")).toHaveLength(0);
58
+ });
59
+ (0, vitest_1.it)("⚠ canSendEmail 是限流器不是权限判定 —— 不得收(§38.4 ② 的老反例)", () => {
60
+ (0, vitest_1.expect)(authz("canSendEmail").length).toBeGreaterThan(0);
61
+ });
62
+ (0, vitest_1.it)("⚠ 名字像谓语但动作词不在表内(canRenderPreview)不得收", () => {
63
+ (0, vitest_1.expect)(authz("canRenderPreview").length).toBeGreaterThan(0);
64
+ });
65
+ });
66
+ (0, vitest_1.describe)("§40 C4 容器严格等于鉴权类 × 授权决策/机制方法", () => {
67
+ (0, vitest_1.it)("Auth.allow_publish —— 授权决策本体(verdaccio)", () => {
68
+ (0, vitest_1.expect)(authz("Auth.allow_publish")).toHaveLength(0);
69
+ });
70
+ (0, vitest_1.it)("Auth.allow_access / allow_unpublish / allow_stage 同族", () => {
71
+ (0, vitest_1.expect)(authz("Auth.allow_access")).toHaveLength(0);
72
+ (0, vitest_1.expect)(authz("Auth.allow_unpublish")).toHaveLength(0);
73
+ (0, vitest_1.expect)(authz("Auth.allow_stage")).toHaveLength(0);
74
+ });
75
+ (0, vitest_1.it)("Auth.setLegacyAuthCacheEntry —— 鉴权缓存内部", () => {
76
+ (0, vitest_1.expect)(authz("Auth.setLegacyAuthCacheEntry")).toHaveLength(0);
77
+ });
78
+ (0, vitest_1.it)("Authorization.allow_access 同形", () => {
79
+ (0, vitest_1.expect)(authz("Authorization.allow_access")).toHaveLength(0);
80
+ });
81
+ (0, vitest_1.it)("⚠ Auth.changePassword —— 受保护操作,绝不能压", () => {
82
+ (0, vitest_1.expect)(authz("Auth.changePassword").length).toBeGreaterThan(0);
83
+ });
84
+ /**
85
+ * ⚠ 这里踩到一个「测不出来」的坑,记录清楚免得后人以为 C4 放行了它:
86
+ * `Auth.invalidateToken` **在 C4 层确实被 PROTECTED_VERB(invalidate)拦下**,
87
+ * 但 §39 的 AUTH_PATTERN 里本来就有 `invalidate`(登出/会话失效入口),
88
+ * 且它按 identifierParse **逐词**匹配 ⇒ "invalidateToken" 拆出 invalidate ⇒ 先命中。
89
+ * 所以整条链路的结果是"压掉",但**不是 C4 压的**。
90
+ * ⇒ 改成直接断言 C4 这一层,避免把别人的行为记到自己账上。
91
+ */
92
+ (0, vitest_1.it)("⚠ Auth.invalidateToken —— C4 层必须拦下(受保护动词)", () => {
93
+ (0, vitest_1.expect)((0, protocol_detector_1.isAuthDecisionName)("Auth.invalidateToken")).toBe(false);
94
+ });
95
+ (0, vitest_1.it)("⚠ Auth.add_user —— 用户管理操作,绝不能压", () => {
96
+ (0, vitest_1.expect)(authz("Auth.add_user").length).toBeGreaterThan(0);
97
+ });
98
+ (0, vitest_1.it)("⚠ AuthController.deletePendingUserData —— 容器不是裸 Auth,绝不能压(§39 V-D 教训)", () => {
99
+ (0, vitest_1.expect)(authz("AuthController.deletePendingUserData").length).toBeGreaterThan(0);
100
+ });
101
+ (0, vitest_1.it)("⚠ AuthService.getCollabToken 之外的业务方法不受影响(AuthService.updateToken)", () => {
102
+ // AuthService 不在"严格等于裸类名"集合里 ⇒ 只有 §39 的机制动词×对象能救
103
+ (0, vitest_1.expect)(authz("AuthService.updateToken").length).toBeGreaterThan(0);
104
+ });
105
+ });
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * §38(2026-09-25)组合式授权谓语 safeguard —— 正/反向 fixture
5
+ *
6
+ * 为什么必须有这个文件:3.7.28–3.7.31 的正则死循环溜过 CI,根因就是
7
+ * 「新增的正则路径没有任何 fixture 触发过它」。本轮新增的三条 safeguard 同理:
8
+ * 盲测语料(blind-benchmark/generated/,合成项目)里**根本没有** cannot/canRemove
9
+ * 这类 CASL 形态 ⇒ 盲测 2865 条零漂移,但也**一条都没触发**(§38.8)。
10
+ * 没有这里的 fixture,这三条正则就是**零覆盖**进主干的。
11
+ *
12
+ * 每条正对照都对应真值集里人工核对过的真实代码:
13
+ * cannot ← docmost SpaceController.deleteSpace
14
+ * canRemove ← verdaccio StorageViewCommand.deleteEntry
15
+ * validateCanEdit ← docmost AttachmentController.uploadFile
16
+ * checkMediaDeletePermission ← hedgedoc MediaController.deleteMedia
17
+ */
18
+ const vitest_1 = require("vitest");
19
+ const protocol_detector_1 = require("./protocol-detector");
20
+ /** 只看所有权/资源所有权两条(本轮只改了这两条的 safeguards) */
21
+ const own = (calls, fnName) => (0, protocol_detector_1.detectSafeguardViolations)(calls, fnName, "typescript", ["user", "id"], true).filter((v) => /Ownership/i.test(v.rule));
22
+ (0, vitest_1.describe)("§38 组合式授权谓语 —— 正对照(有授权 ⇒ 抑制)", () => {
23
+ (0, vitest_1.it)("cannot(CASL 否定谓语)应抑制 Ownership Check", () => {
24
+ // docmost: if (ability.cannot(SpaceCaslAction.Manage, SpaceCaslSubject.Settings)) throw Forbidden
25
+ (0, vitest_1.expect)(own(["createForUser", "cannot", "deleteSpace"], "deleteSpace")).toHaveLength(0);
26
+ });
27
+ (0, vitest_1.it)("canRemove 应抑制", () => {
28
+ // verdaccio: storage.canRemove(...)
29
+ (0, vitest_1.expect)(own(["canRemove", "removeCachedPackage"], "deleteEntry")).toHaveLength(0);
30
+ });
31
+ (0, vitest_1.it)("canPublish 应抑制", () => {
32
+ // verdaccio: auth.allow_publish({packageName}, user, cb) 的包了一层
33
+ (0, vitest_1.expect)(own(["canPublish", "remove"], "stage")).toHaveLength(0);
34
+ });
35
+ (0, vitest_1.it)("validateCanEdit(带前缀 helper)应抑制", () => {
36
+ // docmost: await this.validateCanEdit(...)
37
+ (0, vitest_1.expect)(own(["validateCanEdit", "uploadFile"], "uploadFile")).toHaveLength(0);
38
+ });
39
+ (0, vitest_1.it)("checkMediaDeletePermission(A″ 校验动词 + Permission)应抑制", () => {
40
+ // hedgedoc: MediaController.deleteMedia
41
+ (0, vitest_1.expect)(own(["checkMediaDeletePermission", "delete"], "deleteMedia")).toHaveLength(0);
42
+ });
43
+ (0, vitest_1.it)("validateSpaceAccess 应抑制", () => {
44
+ // docmost: FavoriteController.removeFavorite → resolveAndValidate → validateSpaceAccess
45
+ (0, vitest_1.expect)(own(["resolveAndValidate", "validateSpaceAccess", "removeFavorite"], "removeFavorite")).toHaveLength(0);
46
+ });
47
+ });
48
+ (0, vitest_1.describe)("§38 —— 负对照(同形噪声 ⇒ 仍须报违规)", () => {
49
+ (0, vitest_1.it)("裸 delete 无任何授权 ⇒ 仍报", () => {
50
+ (0, vitest_1.expect)(own(["deleteSpace"], "deleteSpace").length).toBeGreaterThan(0);
51
+ });
52
+ (0, vitest_1.it)("canSendEmail 是限流器不是权限 ⇒ 仍报", () => {
53
+ // 实测抓到:rateLimiter.canSendEmail(userId) 被第一版误收
54
+ (0, vitest_1.expect)(own(["canSendEmail", "update"], "updateThing").length).toBeGreaterThan(0);
55
+ });
56
+ (0, vitest_1.it)("isRight 是 fp-ts Either 判定 ⇒ 仍报", () => {
57
+ // 实测抓到:hoppscotch 满仓 fp-ts,一次冒出 2 条假转绿
58
+ // ⚠ 函数名必须能触发 Ownership Check 的 trigger(delete/remove/update…系动词)。
59
+ // 用 addUserToTeam 的话 trigger 不命中 ⇒ 恒 0,测不出东西(第一版就写错了)。
60
+ (0, vitest_1.expect)(own(["isRight", "removeMemberFromTeam"], "removeTeamMember").length).toBeGreaterThan(0);
61
+ });
62
+ (0, vitest_1.it)("deleteByUsersWithoutSpaceAccess 是数据删除不是校验 ⇒ 仍报", () => {
63
+ (0, vitest_1.expect)(own(["deleteByUsersWithoutSpaceAccess"], "removeMember").length).toBeGreaterThan(0);
64
+ });
65
+ /**
66
+ * ⚠ 本条在 §40 被**推翻**(2026-09-25),保留在此是为了留下决策痕迹:
67
+ * §38 当时"裸 can 太宽(cancel/candidate 同形)⇒ 不收"只是一个**担心**,没有真值证据;
68
+ * §40 人工核剩余池时发现 verdaccio `publish()` 用的是 `const can = allow(auth, …)` +
69
+ * `can('publish')`、CASL 用 `ability.can(Action.Update, subj)` —— **裸 can 是主流写法**,
70
+ * 而"太宽"的担心可以用**精确匹配**解决(cancel/candidate 根本不等于 can)。
71
+ * ⇒ 现在裸 can 收,但两条护栏必须守住(都在 §40 fixture 里钉死):
72
+ * ① 精确匹配 `^can$` / `.can$`(否则 canSendEmail 拆词出 "can" 会被误收)
73
+ * ② `callsOnly: true`(否则 identifierParse 拆出的词 "can" 会误命中)
74
+ */
75
+ (0, vitest_1.it)("cancel / candidate 不是 can ⇒ 仍报(精确匹配护栏)", () => {
76
+ (0, vitest_1.expect)(own(["cancel", "candidate", "remove"], "removeThing").length).toBeGreaterThan(0);
77
+ });
78
+ });
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * §46 ① 凭签发入口豁免(credential_issuance)—— 语义与对称性
5
+ *
6
+ * 背景:`create_access_token|create_refresh_token|create_jwt_token` 只存在于
7
+ * `Authorization (Unauthenticated Mutation)` 的 auth_check 词表里,`Access` 没有。
8
+ * §45 做集合 diff 时发现过,当时以「签发 ≠ 校验调用者」为由没有补进去。§46 把这三个词
9
+ * 从 auth_check **移出**,单独成立一条 `credential_issuance` safeguard(行为不变,语义自洽)。
10
+ *
11
+ * 本文件钉住三件事:
12
+ * 1. 写类函数调用签发函数 ⇒ 豁免(login/register 入口天然不需要事前认证)
13
+ * 2. 读类函数调用签发函数 ⇒ **仍报**(不对称 —— R66:同形状存在相反真值)
14
+ * 3. 它压的是「入口」而非「认证」:与真正的 auth_check 不是一回事
15
+ *
16
+ * ⚠ 名字必须避开 §39 AUTH_PATTERN:ownName/calls 含 User/Token/Auth 会被
17
+ * 「鉴权机制自身」抢先压掉,测不出本机制(R65 唯一压制来源)。
18
+ */
19
+ const vitest_1 = require("vitest");
20
+ const protocol_detector_1 = require("./protocol-detector");
21
+ /** 只取 Authorization 族,避免别的规则干扰断言 */
22
+ const az = (calls, own) => (0, protocol_detector_1.detectSafeguardViolations)(calls, own, "typescript", ["user"], false, ["AuthUser"])
23
+ .filter((v) => v.rule.startsWith("Authorization"))
24
+ .map((v) => v.rule);
25
+ (0, vitest_1.describe)("§46 ① credential_issuance —— 写类豁免", () => {
26
+ (0, vitest_1.it)("写类 + create_access_token ⇒ 免(登录/注册入口天然无需事前认证)", () => {
27
+ (0, vitest_1.expect)(az(["create_access_token", "save"], "createThing")).toHaveLength(0);
28
+ });
29
+ (0, vitest_1.it)("写类 + create_jwt_token ⇒ 免", () => {
30
+ (0, vitest_1.expect)(az(["create_jwt_token", "save"], "createThing")).toHaveLength(0);
31
+ });
32
+ (0, vitest_1.it)("写类 + create_refresh_token ⇒ 免", () => {
33
+ (0, vitest_1.expect)(az(["create_refresh_token", "save"], "createThing")).toHaveLength(0);
34
+ });
35
+ (0, vitest_1.it)("反向对照:写类 + 无签发/无认证 ⇒ 仍报", () => {
36
+ (0, vitest_1.expect)(az(["save", "insert"], "createThing")).toContain("Authorization (Unauthenticated Mutation)");
37
+ });
38
+ });
39
+ (0, vitest_1.describe)("§46 ① credential_issuance —— 不得对称补进读类(R66)", () => {
40
+ (0, vitest_1.it)("读类 + create_access_token ⇒ 仍报 Unauthenticated Access", () => {
41
+ // 若将来有人按「词表对齐」把 mint 补进 Access 词表,这条会转红 —— 那正是要拦的改动。
42
+ // 理由:调用 create_access_token 说明该函数**在签发**令牌,而不是校验调用者;
43
+ // getRefreshToken(userId) 与 getUserToken(userId) 在这个轴上完全同形。
44
+ (0, vitest_1.expect)(az(["create_access_token", "query"], "getThing")).toContain("Authorization (Unauthenticated Access)");
45
+ });
46
+ (0, vitest_1.it)("读类 + create_jwt_token ⇒ 仍报", () => {
47
+ (0, vitest_1.expect)(az(["create_jwt_token", "query"], "getThing")).toContain("Authorization (Unauthenticated Access)");
48
+ });
49
+ (0, vitest_1.it)("读类 + 无认证 ⇒ 仍报(口径不变)", () => {
50
+ (0, vitest_1.expect)(az(["query", "find"], "getThing")).toContain("Authorization (Unauthenticated Access)");
51
+ });
52
+ });
53
+ (0, vitest_1.describe)("§46 ① 签发 ≠ 认证 —— 机制归属", () => {
54
+ (0, vitest_1.it)("真正的认证调用在读写两类上都豁免(对照)", () => {
55
+ (0, vitest_1.expect)(az(["getSession", "save"], "createThing")).toHaveLength(0);
56
+ (0, vitest_1.expect)(az(["getSession", "query"], "getThing")).toHaveLength(0);
57
+ });
58
+ (0, vitest_1.it)("签发调用只在写类上豁免 ⇒ 两者不是同一类证据", () => {
59
+ const mutation = az(["create_access_token", "save"], "createThing");
60
+ const access = az(["create_access_token", "query"], "getThing");
61
+ (0, vitest_1.expect)(mutation).toHaveLength(0);
62
+ (0, vitest_1.expect)(access.length).toBeGreaterThan(0);
63
+ });
64
+ });
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * §46 ② owner_self_assigned —— 资源属主被赋值为当前主体
5
+ *
6
+ * 背景:`Authorization (Ownership Check)` 报「改了别人的东西」,但合成语料上有 12 条
7
+ * FP,成因一致:函数**新建**资源并把属主设为当前主体
8
+ * (`const item = { ..., ownerId: user.id }`)—— 被操作对象就是自己,谈不上越权。
9
+ *
10
+ * 机制:extract-ir 注入 `__progmune_owner_self_assigned__` 标记(对象字面量里的
11
+ * `<属主字段>: <主体>.id`),Ownership Check 的 safeguard 消费它。
12
+ *
13
+ * 本文件钉住:
14
+ * 1. 有标记 ⇒ 豁免
15
+ * 2. 无标记 ⇒ 仍报(反向对照,防止无条件抑制)
16
+ * 3. **不认**「主体标识作实参」形态 —— 那是 R66 否决的形状(见下)
17
+ *
18
+ * ⚠ R66 的边界(为什么不做 `<调用>(…, user.id, …)`):
19
+ * 真实池 `deletePAT(id, user.uid)` 是 FP(已按属主过滤),
20
+ * 合成语料 `deleteWidget` 里 `canSendEmail(user.id)` 是 TP(user.id 与 deleteWidget 无关)。
21
+ * 同形状、相反真值 ⇒ 主体标识作实参这一轴不具备判别力,不得落地。
22
+ * 只认对象字面量里的**绑定**(`userId:` 后跟主体),不认调用实参。
23
+ */
24
+ const vitest_1 = require("vitest");
25
+ const protocol_detector_1 = require("./protocol-detector");
26
+ const own = (calls, name) => (0, protocol_detector_1.detectSafeguardViolations)(calls, name, "typescript", ["user"], false, ["AuthUser"])
27
+ .filter((v) => v.rule === "Authorization (Ownership Check)");
28
+ const MARK = "__progmune_owner_self_assigned__";
29
+ (0, vitest_1.describe)("§46 ② owner_self_assigned —— 属主即主体", () => {
30
+ (0, vitest_1.it)("带标记 ⇒ 豁免(新建资源属主 = 当前主体)", () => {
31
+ (0, vitest_1.expect)(own(["getUser", "push", MARK], "createTransfer")).toHaveLength(0);
32
+ });
33
+ (0, vitest_1.it)("反向对照:无标记 ⇒ 仍报", () => {
34
+ (0, vitest_1.expect)(own(["getUser", "push"], "createTransfer")).toHaveLength(1);
35
+ });
36
+ (0, vitest_1.it)("反向对照:删除类无标记 ⇒ 仍报(不能被顺带压掉)", () => {
37
+ (0, vitest_1.expect)(own(["getUser", "findIndex", "splice"], "deleteTransfer")).toHaveLength(1);
38
+ });
39
+ });
40
+ (0, vitest_1.describe)("§46 ② 边界 —— 只认绑定形态,不认实参形态(R66)", () => {
41
+ (0, vitest_1.it)("主体标识作实参、但无标记 ⇒ 仍报", () => {
42
+ // deleteWidget(widgetId, user) { canSendEmail(user.id); repo.deleteWidget(widgetId); }
43
+ // 这是真 TP:user.id 与 deleteWidget 无关。没有标记 ⇒ 不得豁免。
44
+ (0, vitest_1.expect)(own(["canSendEmail", "deleteWidget"], "deleteWidget")).toHaveLength(1);
45
+ });
46
+ (0, vitest_1.it)("带标记时豁免,与函数名无关", () => {
47
+ (0, vitest_1.expect)(own(["canSendEmail", "deleteWidget", MARK], "deleteWidget")).toHaveLength(0);
48
+ });
49
+ });
50
+ (0, vitest_1.describe)("§46 ② 既有 ownership 证据不受影响", () => {
51
+ (0, vitest_1.it)("内联属主比较(__progmune_ownership_checked__)仍然豁免", () => {
52
+ (0, vitest_1.expect)(own(["getUser", "__progmune_ownership_checked__"], "deleteTransfer")).toHaveLength(0);
53
+ });
54
+ (0, vitest_1.it)("ownerId 比较写法仍然豁免(不加标记也行)", () => {
55
+ (0, vitest_1.expect)(own(["getUser", "checkOwner"], "deleteTransfer")).toHaveLength(0);
56
+ });
57
+ });
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * §44(2026-09-26)`paramGated` 区分「调用者主体」与「被操作对象」—— 正/反向 fixture
5
+ *
6
+ * 为什么必须有这个文件:§43 已经证明盲测语料对 Authorization 抑制类改动**长期零覆盖**
7
+ * (R62 / R56)。本轮改的是 `paramGated` 这条**门**(6 条规则共用),合成语料里
8
+ * 既没有 `token: string` 这种"被操作对象当身份"的形态,也没有 `user: User` 的对照,
9
+ * ⇒ 光靠闸门会全绿但一条都没触发。这里的 fixture 是它唯一的覆盖。
10
+ *
11
+ * 真值来源(blind-benchmark/reports/paramgate-probe.json,全池 66 条 paramGated 违规):
12
+ * 压掉的 8 条 = 6 verified FP + 2 heuristic FP,**TP 0 / UNKNOWN 0**
13
+ * - updateLastUsedForPAT(token: string) ← 被操作的 PAT(hoppscotch)
14
+ * - resolveRemoteUser(config: Config, token: string)(verdaccio)
15
+ * - verifyJWTPayload(token: string, …) (verdaccio)
16
+ * - setAuthCookie(res: FastifyReply, token: string)(docmost)
17
+ * 保留的(反例侧):
18
+ * - removeUserAvatar(user: User) / createPAT(dto, user: AuthUser) ← 主体是领域对象
19
+ * - JwtAuthGuard.handleRequest(err: any, user: any, …) ← 类型解析不出来 ⇒ 保守放行
20
+ */
21
+ const vitest_1 = require("vitest");
22
+ const protocol_detector_1 = require("./protocol-detector");
23
+ /** 只看 4 条 Authorization(本轮 paramGated 收紧影响的就是这几条) */
24
+ const authz = (calls, fnName, params, paramTypes, exposed = false) => (0, protocol_detector_1.detectSafeguardViolations)(calls, fnName, "typescript", params, exposed, paramTypes).filter((v) => v.category === "authorization");
25
+ (0, vitest_1.describe)("§44 classifyParamType —— 三分类", () => {
26
+ (0, vitest_1.it)("标量 ⇒ scalar", () => {
27
+ (0, vitest_1.expect)((0, protocol_detector_1.classifyParamType)("string")).toBe("scalar");
28
+ (0, vitest_1.expect)((0, protocol_detector_1.classifyParamType)("number")).toBe("scalar");
29
+ (0, vitest_1.expect)((0, protocol_detector_1.classifyParamType)("boolean")).toBe("scalar");
30
+ (0, vitest_1.expect)((0, protocol_detector_1.classifyParamType)("string[]")).toBe("scalar");
31
+ });
32
+ (0, vitest_1.it)("领域类型 ⇒ typed", () => {
33
+ (0, vitest_1.expect)((0, protocol_detector_1.classifyParamType)("User")).toBe("typed");
34
+ (0, vitest_1.expect)((0, protocol_detector_1.classifyParamType)("AuthUser")).toBe("typed");
35
+ (0, vitest_1.expect)((0, protocol_detector_1.classifyParamType)("Admin")).toBe("typed");
36
+ (0, vitest_1.expect)((0, protocol_detector_1.classifyParamType)("ExecutionContext")).toBe("typed");
37
+ });
38
+ (0, vitest_1.it)("解析不出来 ⇒ unknown(保守,不收紧)", () => {
39
+ (0, vitest_1.expect)((0, protocol_detector_1.classifyParamType)("any")).toBe("unknown");
40
+ (0, vitest_1.expect)((0, protocol_detector_1.classifyParamType)("unknown")).toBe("unknown");
41
+ (0, vitest_1.expect)((0, protocol_detector_1.classifyParamType)("")).toBe("unknown");
42
+ (0, vitest_1.expect)((0, protocol_detector_1.classifyParamType)("null")).toBe("unknown");
43
+ });
44
+ (0, vitest_1.it)("联合类型按「含领域类型即 typed」处理", () => {
45
+ (0, vitest_1.expect)((0, protocol_detector_1.classifyParamType)("User | null")).toBe("typed");
46
+ (0, vitest_1.expect)((0, protocol_detector_1.classifyParamType)("AuthenticationType | null")).toBe("typed");
47
+ (0, vitest_1.expect)((0, protocol_detector_1.classifyParamType)("string | null")).toBe("scalar");
48
+ // 含 any 的联合不敢判标量 ⇒ typed(保守放行)
49
+ (0, vitest_1.expect)((0, protocol_detector_1.classifyParamType)("any | User")).toBe("typed");
50
+ });
51
+ });
52
+ (0, vitest_1.describe)("§44 正对照 —— 唯一的身份线索是标量 ⇒ 那是被操作对象,不是调用者", () => {
53
+ (0, vitest_1.it)("updateLastUsedForPAT(token: string) 应被 paramGated 挡下", () => {
54
+ // hoppscotch:被操作的是要更新 lastUsed 的那个 PAT,不是调用者凭证
55
+ (0, vitest_1.expect)(authz(["extractUUID", "left", "update", "right", "cast"], "updateLastUsedForPAT", ["token"], ["string"])).toHaveLength(0);
56
+ });
57
+ // ⚠ 两条曾用真名 `resolveRemoteUser` / `verifyJWTPayload`,反向验证时**没转红**
58
+ // ⇒ 它们是被 safeguard(`jwtVerify` / 自身名里的 `verify`)压掉的,不是被
59
+ // paramGated 压掉的 —— 按 R64/R65 是**假绿**,必须换成能真正压在门上的形态。
60
+ (0, vitest_1.it)("updateRemoteUser(config: Config, token: string) 应被挡下", () => {
61
+ // 形态同 verdaccio resolveRemoteUser:token 是待解析的入参,不是调用者凭证
62
+ (0, vitest_1.expect)(authz(["readFileSync", "split", "at"], "updateRemoteUser", ["config", "token"], ["Config", "string"])).toHaveLength(0);
63
+ });
64
+ // ⚠ 第二版曾用 `rotateSecretValue` —— 反向验证也没转红:`rotate` 不在
65
+ // Authorization 的 trigger 动词表里,规则压根没触发 ⇒ 恒绿,还是假绿。
66
+ (0, vitest_1.it)("updateSecretValue(token: string, secret: string) 应被挡下", () => {
67
+ (0, vitest_1.expect)(authz(["split", "at", "concat"], "updateSecretValue", ["token", "secret"], ["string", "string"])).toHaveLength(0);
68
+ });
69
+ });
70
+ (0, vitest_1.describe)("§44 反向对照 —— 主体参数必须仍然放行(不能变成无条件抑制)", () => {
71
+ (0, vitest_1.it)("removeUserAvatar(user: User) 仍应报 —— 主体是领域对象", () => {
72
+ (0, vitest_1.expect)(authz(["getAttachmentFolderPath", "deleteRedundantFile", "updateUser"], "removeUserAvatar", ["user"], ["User"])).not.toHaveLength(0);
73
+ });
74
+ (0, vitest_1.it)("createPAT(dto, user: AuthUser) 仍应报", () => {
75
+ (0, vitest_1.expect)(authz(["isValidLength", "create", "cast"], "createPAT", ["dto", "user"], ["CreateAccessTokenDto", "AuthUser"])).not.toHaveLength(0);
76
+ });
77
+ (0, vitest_1.it)("user: any 仍应报 —— 类型解析不出来时保守放行(R57:退回旧行为,不是一律压)", () => {
78
+ // ⚠ 函数名不能用 generateJWT —— 它是 isAuthFunctionName 的鉴权机制自身,
79
+ // authorization 规则对它整体跳过,测不出 paramGated 的门。
80
+ (0, vitest_1.expect)(authz(["findOne", "save"], "updateProfile", ["user"], ["any"])).not.toHaveLength(0);
81
+ });
82
+ (0, vitest_1.it)("user 是标量 string 但函数 exposed ⇒ 仍应报(exposed 是独立的入口证据)", () => {
83
+ (0, vitest_1.expect)(authz(["update", "save"], "updateProfile", ["user"], ["string"], true)).not.toHaveLength(0);
84
+ });
85
+ (0, vitest_1.it)("不传 paramTypes ⇒ 完全保持旧行为(零漂移)", () => {
86
+ // 旧行为:参数名命中 identity 词表即通过门 ⇒ token 也放行
87
+ (0, vitest_1.expect)(authz(["update", "save"], "updateLastUsedForPAT", ["token"])).not.toHaveLength(0);
88
+ });
89
+ });
90
+ (0, vitest_1.describe)("§44 IDENTITY_PARAM_RE —— 与旧内联正则同源", () => {
91
+ (0, vitest_1.it)("命中身份词(必须是**独立单词**)", () => {
92
+ for (const n of ["user", "token", "session", "role", "request", "credential", "identity"]) {
93
+ (0, vitest_1.expect)(protocol_detector_1.IDENTITY_PARAM_RE.test(n)).toBe(true);
94
+ }
95
+ });
96
+ (0, vitest_1.it)("复合词一律不算 —— 词边界使然,这是**既有行为**不是本轮引入", () => {
97
+ // ⚠ 顺带记一笔项目债:最地道的 NestJS 主体参数名 `authUser` / `currentUser`
98
+ // **也不命中**(\buser\b 在 authUser 里没有左边界)。也就是说 paramGated
99
+ // 目前只认 `user` / `token` / `auth` 这类裸标识符。本轮不改(避免与收紧
100
+ // 混在一起),但它是"主体识别"这条轴的下一处缺口。
101
+ for (const n of ["userEmail", "userId", "userRepo", "authUser", "currentUser", "req", "ctx"]) {
102
+ (0, vitest_1.expect)(protocol_detector_1.IDENTITY_PARAM_RE.test(n)).toBe(false);
103
+ }
104
+ });
105
+ });
@@ -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|create_access_token|create_refresh_token|create_jwt_token|__progmune_auth_checked__|__progmune_credential_check__|__progmune_drf_permissions__|__progmune_auth_machinery__)\b/i, label: "auth_check" },
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"],
@@ -528,6 +603,12 @@ const SAFEGUARD_RULES = [
528
603
  safeguards: [
529
604
  { pattern: /\b(ownerId\s*[!=]==?|authorId\s*[!=]==?|userId\s*[!=]==?|createdBy|\.owner\s*[!=]==?)/i, label: "ownership_comparison" },
530
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 },
531
612
  ],
532
613
  violationMessage: "Resource mutation checks authentication but does NOT verify the resource belongs to the requesting user. Missing ownerId/authorId comparison.",
533
614
  conceptMissing: ["ResourceOwnership", "HorizontalAuthorization"],
@@ -1138,11 +1219,200 @@ function identifierParse(name) {
1138
1219
  }
1139
1220
  return words;
1140
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
+ }
1141
1377
  /**
1142
1378
  * Detect missing safeguards in function call sequences.
1143
1379
  * Uses identifier parsing to match compound names (registerNewUser → register).
1144
1380
  */
1145
- function detectSafeguardViolations(calls, enclosingFuncName, language, params, exposed) {
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) {
1146
1416
  const violations = [];
1147
1417
  // Build effective calls: raw names + identifier-parsed words.
1148
1418
  // Class-qualified names (Class.method) contribute only their METHOD name —
@@ -1156,10 +1426,8 @@ function detectSafeguardViolations(calls, enclosingFuncName, language, params, e
1156
1426
  }
1157
1427
  const effectiveCalls = [...new Set([...rawCalls, ...parsedWords])];
1158
1428
  // Skip authorization rules for auth functions — check both raw lowercased name and parsed words
1159
- const rawLower = enclosingFuncName?.toLowerCase() || "";
1160
- 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;
1161
- const isAuthFunction = enclosingFuncName != null && (AUTH_PATTERN.test(rawLower) ||
1162
- identifierParse(enclosingFuncName).some(w => AUTH_PATTERN.test(w)));
1429
+ // §39:入口词表 ∪ 鉴权机制自身(自指排除),详见 isAuthFunctionName
1430
+ const isAuthFunction = isAuthFunctionName(enclosingFuncName);
1163
1431
  // Filter rules by language
1164
1432
  const activeRules = language
1165
1433
  ? SAFEGUARD_RULES.filter(r => !r.languages || r.languages.includes(language))
@@ -1197,8 +1465,23 @@ function detectSafeguardViolations(calls, enclosingFuncName, language, params, e
1197
1465
  // Surface gate (paramGated): only apply to functions that can plausibly
1198
1466
  // authenticate — routed by a web handler (exposed) or taking an
1199
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)。
1200
1475
  if (rule.paramGated && params) {
1201
- const hasIdentity = params.some(p => /\b(token|session|user|auth|request|scope|cookie|credential|permission|role|identity)\b/i.test(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
+ });
1202
1485
  if (!hasIdentity && !exposed)
1203
1486
  continue;
1204
1487
  }
@@ -1311,10 +1594,8 @@ function detectSafeguardViolationsV7(calls, enclosingFuncName, callerMap, funcCa
1311
1594
  for (const c of triggerEffectiveCalls)
1312
1595
  safeContext.add(c);
1313
1596
  // Skip authorization rules for auth functions
1314
- const rawLower = enclosingFuncName?.toLowerCase() || "";
1315
- 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;
1316
- const isAuthFunction = enclosingFuncName != null && (AUTH_PATTERN.test(rawLower) ||
1317
- identifierParse(enclosingFuncName).some(w => AUTH_PATTERN.test(w)));
1597
+ // §39:入口词表 ∪ 鉴权机制自身(自指排除),详见 isAuthFunctionName
1598
+ const isAuthFunction = isAuthFunctionName(enclosingFuncName);
1318
1599
  // Filter rules by language
1319
1600
  const activeRulesV7 = language
1320
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.51";
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.52",
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/**'",