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.
@@ -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
+ });