progmune-runtime 3.7.52 → 3.7.54

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,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
+ });
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * §47:签名级会话证据 —— 参数类型以 `Session` 结尾 ⇒ 「未认证」结论不成立。
5
+ *
6
+ * 为什么不是继续在 calls 里找 auth 词:hedgedoc 的 pending-user 三个接口 +
7
+ * OidcController.callback 都是 `@UseGuards(SessionGuard)` 保护的会话类路由,
8
+ * 但它们**一个认证函数都不调用** —— 身份是从 `request.session.*` 上取的,
9
+ * 调用列表里只有 property access,选择器读不到。真实池里这四条全是人工确认的 FP。
10
+ *
11
+ * 改用**参数类型**:签名上存在 session 对象 ⇒ 这次调用必然落在某个会话上下文里。
12
+ * 好处是它来自 AST 签名,不依赖调用图连通(fp-pool 入边 0 的函数占 36%~42%,
13
+ * 依赖 calls 的机制在那里会静默失效)。
14
+ *
15
+ * ⚠ R66 的否命题在本文件 §47 反向② 组:
16
+ * 有会话 ≠ 操作对象属于自己。越权读取的同形状反例必须 Unauthenticated 熄灭
17
+ * 而 **Ownership 继续亮**,否则这一刀就是把越权漏洞洗白。
18
+ */
19
+ const vitest_1 = require("vitest");
20
+ const protocol_detector_1 = require("./protocol-detector");
21
+ const ACC = "Authorization (Unauthenticated Access)";
22
+ const MUT = "Authorization (Unauthenticated Mutation)";
23
+ const OWN = "Authorization (Ownership Check)";
24
+ const RES = "Authorization (Resource Ownership)";
25
+ /** lang 固定 typescript;params/paramTypes 逐用例给定 */
26
+ function fire(calls, name, params, paramTypes) {
27
+ const vs = (0, protocol_detector_1.detectSafeguardViolations)(calls, name, "typescript", params, false, paramTypes);
28
+ return new Set(vs.map((v) => v.rule));
29
+ }
30
+ const lit = (b) => (b ? "fires" : "silent");
31
+ (0, vitest_1.describe)("§47 正向:签名带会话 ⇒ Unauthenticated 两条熄灭(hedgedoc 真实 FP)", () => {
32
+ (0, vitest_1.it)("getPendingUserData(request: RequestWithSession) 不再报未认证读取", () => {
33
+ const s = fire(["create"], "getPendingUserData", ["request"], ["RequestWithSession"]);
34
+ (0, vitest_1.expect)(lit(s.has(ACC))).toBe("silent");
35
+ });
36
+ (0, vitest_1.it)("confirmPendingUserData(request: RequestWithSession, dto) 不再报未认证变更", () => {
37
+ const s = fire(["createUserWithIdentityFromPendingUserConfirmation", "save"], "confirmPendingUserData", ["request", "pendingUserConfirmationData"], ["RequestWithSession", "PendingUserConfirmationDto"]);
38
+ (0, vitest_1.expect)(lit(s.has(MUT))).toBe("silent");
39
+ });
40
+ (0, vitest_1.it)("OidcController.callback —— session 不在第一个参数也算数", () => {
41
+ const s = fire(["extractUserInfoFromCallback", "getExistingOidcIdentity", "mayUpdateIdentity", "save"], "OidcController.callback", ["oidcIdentifier", "request"], ["string", "RequestWithSession"]);
42
+ (0, vitest_1.expect)(lit(s.has(ACC))).toBe("silent");
43
+ (0, vitest_1.expect)(lit(s.has(MUT))).toBe("silent");
44
+ });
45
+ (0, vitest_1.it)("裸 Session 类型同样成立(不要求 Request 前缀)", () => {
46
+ const s = fire(["find"], "getProfile", ["session"], ["Session"]);
47
+ (0, vitest_1.expect)(lit(s.has(ACC))).toBe("silent");
48
+ });
49
+ });
50
+ (0, vitest_1.describe)("§47 反向①:摘掉这一刀必须重新亮(证明是它在压,不是别机制抢先)", () => {
51
+ (0, vitest_1.it)("同函数把 RequestWithSession 换成 Request ⇒ 未认证读取重新报", () => {
52
+ const s = fire(["create"], "getPendingUserData", ["request"], ["Request"]);
53
+ (0, vitest_1.expect)(lit(s.has(ACC))).toBe("fires");
54
+ });
55
+ // 注意这里**不是** §47 在压:`request` 的类型退化成标量后,是 §44 的 paramGated
56
+ // 先把规则整体跳过了(标量参数不算身份证据)。单独留一条是为了把两层闸门的关系钉住:
57
+ // 类型通道同时喂给 paramGate(§44)和 paramTypeSafeguards(§47),
58
+ // 断言这一条可以防止以后有人误以为「类型一变就该亮」。
59
+ (0, vitest_1.it)("request:string 时由 §44 paramGate 先行跳过 —— 记录两层闸门的先后关系", () => {
60
+ const s = fire(["createUserWithIdentityFromPendingUserConfirmation", "save"], "confirmPendingUserData", ["request", "pendingUserConfirmationData"], ["string", "PendingUserConfirmationDto"]);
61
+ (0, vitest_1.expect)(lit(s.has(MUT))).toBe("silent");
62
+ });
63
+ (0, vitest_1.it)("不传类型通道时退回旧行为(仍报)—— R57 要求通道不可用时不得静默压", () => {
64
+ const vs = (0, protocol_detector_1.detectSafeguardViolations)(["create"], "getPendingUserData", "typescript", ["request"]);
65
+ (0, vitest_1.expect)(lit(vs.some((v) => v.rule === ACC))).toBe("fires");
66
+ });
67
+ });
68
+ (0, vitest_1.describe)("§47 反向②:有会话 ≠ 有权 —— 越权形态的 Unauthenticated 熄灭但 Ownership 必须继续亮", () => {
69
+ (0, vitest_1.it)("removeSharedFile(req: RequestWithSession, id) —— 归属检查不能随会话一起被免掉", () => {
70
+ const s = fire(["findById", "removeFile"], "removeSharedFile", ["request", "id"], ["RequestWithSession", "string"]);
71
+ (0, vitest_1.expect)(lit(s.has(MUT))).toBe("silent");
72
+ (0, vitest_1.expect)(lit(s.has(OWN))).toBe("fires");
73
+ });
74
+ (0, vitest_1.it)("deleteOtherUsersNote(session: Session, noteId) —— Resource Ownership 必须继续亮", () => {
75
+ const s = fire(["findNote", "deleteNote"], "deleteOtherUsersNote", ["session", "noteId"], ["Session", "string"]);
76
+ (0, vitest_1.expect)(lit(s.has(MUT))).toBe("silent");
77
+ (0, vitest_1.expect)(lit(s.has(OWN) || s.has(RES))).toBe("fires");
78
+ });
79
+ });
80
+ (0, vitest_1.describe)("§47 边界:类型名不得以 Session 收尾就被误当成会话", () => {
81
+ const cases = [
82
+ ["Transaction", " mutation"],
83
+ ["SessionOptions", "配置对象不是会话本身"],
84
+ ["Partial<AuthConf>", "verdaccio addAuth 的真类型"],
85
+ ["Knex", "查询构造器"],
86
+ ];
87
+ for (const [type, why] of cases) {
88
+ (0, vitest_1.it)(`${type} 不应被当作会话(${why.trim()})`, () => {
89
+ const s = fire(["merge"], "addAuth", ["auth"], [type]);
90
+ (0, vitest_1.expect)(lit(s.has(MUT))).toBe("fires");
91
+ });
92
+ }
93
+ });