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.
@@ -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,64 @@ 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
+ */
317
+ /**
318
+ * §47(2026-09-27)**会话承载型请求**:参数类型名以 `Session` 结尾
319
+ * (`RequestWithSession` / `AuthenticatedSession` / `HttpSession` / `Session`)。
320
+ *
321
+ * 语义:函数手里握着一个 session 对象 ⇒ 这次调用**已经落在某个会话上下文里**,
322
+ * 「任何人无需凭证即可访问/修改」这个结论不成立。这是**正向**证据且来自 AST 签名,
323
+ * 不像「calls 里没有 xxx」那样会被提取失败伪装过去(fp-pool 入边为 0 的函数占
324
+ * 36%~42%,依赖 calls 的机制在那些函数上会静默失效)。
325
+ *
326
+ * ⚠ 只喂给「未认证」两条规则,**不**给 Ownership / Resource Ownership 补 ——
327
+ * 有会话 ≠ 操作对象属于自己。R66 的否命题由这对组合承担:
328
+ * 同形状的越权读取 `getOthersData(req: RequestWithSession)` 应当
329
+ * Unauthenticated 熄灭、**Ownership 继续亮**(fixture 见
330
+ * src/protocol-detector-session-param.test.ts 的反向组)。
331
+ */
332
+ const SESSION_BEARING_TYPE_RE = /(?:^|[A-Za-z])Session$/i;
270
333
  const SAFEGUARD_RULES = [
271
334
  // ── Password Hashing ──
272
335
  {
@@ -322,6 +385,20 @@ const SAFEGUARD_RULES = [
322
385
  { pattern: /\b(checkOwner|isOwner|ownerId\s*[!=]==?|authorId\s*[!=]==?|userId\s*[!=]==?|createdBy\s*[!=]==?|\.owner\s*[!=]==?|\.user\s*[!=]==?)\b/i, label: "ownership_check" },
323
386
  { pattern: /\b(hasPermission|checkPermission|checkAccess|isAuthorized|checkRole|requireRole|adminCheck|isAdmin|canModify|canDelete|canEdit)\b/i, label: "authz_check" },
324
387
  { pattern: /\b(__progmune_ownership_checked__)\b/, label: "inline_ownership_check" },
388
+ // §46 ②:资源属主被**赋值为当前主体**(`{ userId: user.id }` / `where:{userId: user.id}`)。
389
+ // 这类函数的被操作对象就是(或已被限定在)主体自己,谈不上「改别人的东西」。
390
+ // 合成语料上覆盖 10/10 FP(createTransfer / createRefund / uploadFile / placeOrder…)。
391
+ // ⚠ 只认 `<属主字段>: <主体>.id` 的**绑定**形态;**不认** `<调用>(…, user.id, …)` ——
392
+ // 主体标识作实参这一形状实测存在相反真值(deletePAT 是 FP,deleteWidget 是 TP),按 R66 不落地。
393
+ { pattern: /\b(__progmune_owner_self_assigned__)\b/, label: "owner_self_assigned" },
394
+ // §38(2026-09-25)CASL / 组合式授权谓语。上面两条是**动词白名单**,
395
+ // 而真实世界的谓语是 `can|cannot|may` + 动作 的**组合式**,
396
+ // 白名单穷举不完(canRemove/canManage/canPublish…)。三条规则见 §38.3。
397
+ { pattern: exports.AUTHZ_PREDICATE_RE, label: "casl_predicate" },
398
+ { pattern: exports.AUTHZ_CAN_HELPER_RE, label: "can_helper" },
399
+ { pattern: exports.AUTHZ_ACCESS_CHECK_RE, label: "access_check" },
400
+ // §40 C1:裸 can(CASL ability.can / verdaccio can('publish'))—— 精确匹配
401
+ { pattern: exports.AUTHZ_BARE_CAN_RE, label: "bare_can", callsOnly: true },
325
402
  ],
326
403
  violationMessage: "Mutation operation does not verify that the acting user owns the resource or holds the required permission before modifying data.",
327
404
  conceptMissing: ["OwnershipCheck", "AuthorizationGuard"],
@@ -346,9 +423,13 @@ const SAFEGUARD_RULES = [
346
423
  category: "authorization",
347
424
  languages: ["typescript", "javascript", "python"],
348
425
  paramGated: true,
426
+ // §47:签名带 session ⇒ 已落在会话上下文中(详见 SESSION_BEARING_TYPE_RE 注释)
427
+ paramTypeSafeguards: [SESSION_BEARING_TYPE_RE],
349
428
  trigger: /\b(list|download|view|fetch)(?:[A-Z]\w*|_\w+)|get(?:[A-Z]\w+|_\w+)/i,
350
429
  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" },
430
+ { 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" },
431
+ // §40 C1:裸 can —— verdaccio 的 can('publish') 中间件就是这里的授权判定
432
+ { pattern: exports.AUTHZ_BARE_CAN_RE, label: "bare_can", callsOnly: true },
352
433
  ],
353
434
  violationMessage: "Data access function does not check authentication. Anyone can access data without credentials.",
354
435
  conceptMissing: ["AuthenticationCheck", "AccessControl"],
@@ -376,11 +457,25 @@ const SAFEGUARD_RULES = [
376
457
  category: "authorization",
377
458
  languages: ["typescript", "javascript", "python"],
378
459
  paramGated: true,
460
+ // §47:同上 —— 只压制「未认证」,不压制「越权」
461
+ paramTypeSafeguards: [SESSION_BEARING_TYPE_RE],
379
462
  // Note: "post" deliberately excluded — it collides with the Post entity name
380
463
  // (listPosts/getPost/deletePost fire via identifier-parsed words).
381
464
  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
465
  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" },
466
+ { 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" },
467
+ // §40 C1:裸 can —— 同上
468
+ { pattern: exports.AUTHZ_BARE_CAN_RE, label: "bare_can", callsOnly: true },
469
+ // §46 ①:凭签发入口豁免 —— 独立 safeguard,**不并入 auth_check**。
470
+ // create_*_token 是「签发令牌」不是「校验调用者身份」,两者语义相反:
471
+ // 调用 create_access_token 恰恰说明该函数**没有**校验任何人的身份(它就是发令牌的地方)。
472
+ // 之所以仍要豁免,是因为 login / register / issueToken 这类入口天然不需要事前认证
473
+ // (这里压的是「登录接口被判未认证变更」那批 FP),不是因为它做了认证。
474
+ // ⚠ 因此它**不得对称补进 Unauthenticated Access**(R66:同形状存在相反真值 ——
475
+ // getRefreshToken(userId) 是该免的签发入口,getUserToken(userId) 是不该免的越权读取,
476
+ // 单函数视角区分不了)。分开成两条 label 就是为了让下一次词表 diff(R69)
477
+ // 不再把这三个词当成 auth_check 的「缺项」而机械对齐过去。
478
+ { pattern: /\b(create_access_token|create_refresh_token|create_jwt_token)\b/i, label: "credential_issuance" },
384
479
  ],
385
480
  violationMessage: "Mutation function does not check authentication. Anyone can create or modify data without credentials.",
386
481
  conceptMissing: ["AuthenticationCheck", "AccessControl"],
@@ -528,6 +623,12 @@ const SAFEGUARD_RULES = [
528
623
  safeguards: [
529
624
  { pattern: /\b(ownerId\s*[!=]==?|authorId\s*[!=]==?|userId\s*[!=]==?|createdBy|\.owner\s*[!=]==?)/i, label: "ownership_comparison" },
530
625
  { pattern: /\b(__progmune_ownership_checked__)\b/, label: "inline_ownership_check" },
626
+ // §38:同 Ownership Check,组合式谓语一并接上(这条规则更严,更需要)
627
+ { pattern: exports.AUTHZ_PREDICATE_RE, label: "casl_predicate" },
628
+ { pattern: exports.AUTHZ_CAN_HELPER_RE, label: "can_helper" },
629
+ { pattern: exports.AUTHZ_ACCESS_CHECK_RE, label: "access_check" },
630
+ // §40 C1:同上
631
+ { pattern: exports.AUTHZ_BARE_CAN_RE, label: "bare_can", callsOnly: true },
531
632
  ],
532
633
  violationMessage: "Resource mutation checks authentication but does NOT verify the resource belongs to the requesting user. Missing ownerId/authorId comparison.",
533
634
  conceptMissing: ["ResourceOwnership", "HorizontalAuthorization"],
@@ -1138,11 +1239,200 @@ function identifierParse(name) {
1138
1239
  }
1139
1240
  return words;
1140
1241
  }
1242
+ /**
1243
+ * §39(2026-09-25)D 类自指排除:把「鉴权机制自身」也识别为 auth function。
1244
+ *
1245
+ * 引擎原有 AUTH_PATTERN 只列了**用户鉴权入口**(login / register / logout / …),
1246
+ * 没列**鉴权机制自身**,于是对下面这类函数报「未鉴权」,属自指谬误:
1247
+ * JwtAuthGuard.handleRequest / TokenService.verifyJwt / AuthService.getCollabToken
1248
+ * / WorkspaceAbilityFactory.createForUser(CASL 授权引擎本体)
1249
+ *
1250
+ * 判据是**结构化**的两条,不是再往里加词(R55:组合规则必须带约束):
1251
+ *
1252
+ * ① 机制类容器 = 机制后缀(Guard|Strategy|AbilityFactory) **×** 凭据名词
1253
+ * ⚠ 只看后缀不够:`Strategy` 是通用设计模式后缀,CacheStrategy /
1254
+ * PaymentStrategy 会被误吃;`Guard` 同理(NestJS 的 ThrottlerGuard 是
1255
+ * 限流不是鉴权)。加凭据名词约束后这两类都排除。
1256
+ * ② 方法 = 机制动词 × 机制对象,**只看点号后的方法部分**
1257
+ * (容器词不参与动词匹配,否则 ApiTokensController 会漏进来)
1258
+ *
1259
+ * ⚠⚠ 边界(实测踩到的越界,别再放宽):
1260
+ * - **不得**改成「容器含 token/session 即算」。实测那样会吃掉
1261
+ * `ApiTokensController.deleteToken` / `AccessTokenController.deletePAT` /
1262
+ * `AccessTokenService.updateLastUsedForPAT` 的 Ownership Check ——
1263
+ * 对**凭据做增删改**恰恰是最需要鉴权的地方,压掉就是真漏报且不可见。
1264
+ * - 机制对象**不含** password(changePassword/passwordReset 是受保护操作)
1265
+ * - 机制对象**不含** pat(hoppscotch 产品专有缩写,收它会吃掉 createPAT,
1266
+ * 属过拟合到单个仓库的缩写)
1267
+ */
1268
+ const AUTH_MACHINERY_CLASS_SUFFIX = /(?:Guard|Strategy|AbilityFactory)$/;
1269
+ const AUTH_MACHINERY_NOUN_WORDS = [
1270
+ "auth", "jwt", "token", "session", "cookie", "credential", "passport",
1271
+ "identity", "oauth", "oidc", "ldap", "saml", "bearer", "apikey", "ability", "permission",
1272
+ ];
1273
+ const AUTH_MACHINERY_VERB_WORDS = [
1274
+ "verify", "validate", "sign", "generate", "issue", "decode", "parse",
1275
+ "encode", "refresh", "rotate", "get", "set", "create", "build", "make", "write", "read",
1276
+ ];
1277
+ const AUTH_MACHINERY_OBJECT_WORDS = [
1278
+ "jwt", "token", "cookie", "session", "credential", "credentials", "signature", "bearer",
1279
+ ];
1280
+ /**
1281
+ * §41 E1:token **内嵌**的 acronym 前缀 —— JWT from `JWTPayload`。
1282
+ *
1283
+ * 背景(§40.8 候选 C5):`identifierParse("verifyJWTPayload")` → ["verify","JWTPayload"],
1284
+ * "JWTPayload" 整块 ≠ "jwt" ⇒ 凭据语义对**任何**词表都不可见。
1285
+ *
1286
+ * ⚠⚠ 为什么**不改 identifierParse 主干**(C5 原案的处置结论,2026-09-25):
1287
+ * identifierParse 有 **5 个调用点**,其中 `triggerParsedWords` 供给 **trigger 匹配**、
1288
+ * `effectiveCalls` 供给 **safeguard 匹配** —— 动它等于同时改所有规则的触发面与抑制面。
1289
+ * `split-probe.ts` 实测:母本 803 个去重标识符里只有 **16 个**受影响,新增 22 个词中
1290
+ * 仅 **2 个**(JWT / OIDC)是机制类,其余全是噪声:
1291
+ * OAuthToken → +"Auth" (从 OAuth 里拆出 Auth,污染面极大)
1292
+ * throwHTTPErr → +"HTTP" +"Err"
1293
+ * createATeam → +"A" +"Team"
1294
+ * prosemirrorNodeToYElement → +"Y" +"Element"
1295
+ * ⇒ 收益 2 条 vs 全局 trigger/effectiveCalls 漂移 ⇒ **主干不改**。
1296
+ *
1297
+ * 折中:**只在「这个函数是否是鉴权机制」这一处**补认 acronym ——
1298
+ * 局部 ⇒ 不影响 triggerParsedWords / effectiveCalls ⇒ **结构上不可能造成 LOST**。
1299
+ */
1300
+ const ACRONYM_PREFIX = /^([A-Z]{2,})(?=[A-Z][a-z])/;
1301
+ /**
1302
+ * 单个 token 里嵌的 acronym:JWTPayload→JWT / OIDCStrategy→OIDC / HTTPErr→HTTP。
1303
+ * ⚠ `{2,}` 与贪婪回溯两条约束是实测出来的,别随手放宽:
1304
+ * - `{2,}`:否则 `OAuth` → "O"(satisfies O|Auth 的 lookahead),单字母进词表 = 灾难;
1305
+ * - 贪婪回溯:`OIDCStrategy` 必须拿到 "OIDC" 而不是 "OID"
1306
+ * (非贪婪 `[A-Z]+?` 会停在 OID,"oid" 不在任何机制词表里 ⇒ 这条救不了)。
1307
+ * - lookahead `[A-Z][a-z]`:纯 PascalCase 词(Token/Payload)不匹配,避免把自己再切一遍。
1308
+ */
1309
+ function tokenAcronyms(token) {
1310
+ const m = ACRONYM_PREFIX.exec(token);
1311
+ return m ? [m[1]] : [];
1312
+ }
1313
+ /** identifierParse 的词 + 每个词内嵌的 acronym,统一小写 */
1314
+ function machineryWords(method) {
1315
+ const base = identifierParse(method);
1316
+ const out = base.map((w) => w.toLowerCase());
1317
+ for (const t of base)
1318
+ for (const a of tokenAcronyms(t))
1319
+ out.push(a.toLowerCase());
1320
+ return out;
1321
+ }
1322
+ function isAuthMachineryName(enclosingFuncName) {
1323
+ const dot = enclosingFuncName.lastIndexOf(".");
1324
+ const container = dot > 0 ? enclosingFuncName.slice(0, dot) : "";
1325
+ // ① 机制类容器
1326
+ if (container && AUTH_MACHINERY_CLASS_SUFFIX.test(container)) {
1327
+ const cl = container.toLowerCase();
1328
+ if (AUTH_MACHINERY_NOUN_WORDS.some((w) => cl.includes(w)))
1329
+ return true;
1330
+ }
1331
+ // ② 方法 = 机制动词 × 机制对象(§41 E1:对象侧支持内嵌 acronym,见 ACRONYM_PREFIX 注释)
1332
+ const method = dot >= 0 ? enclosingFuncName.slice(dot + 1) : enclosingFuncName;
1333
+ const mw = machineryWords(method);
1334
+ if (mw.some((w) => AUTH_MACHINERY_VERB_WORDS.includes(w)) &&
1335
+ mw.some((w) => AUTH_MACHINERY_OBJECT_WORDS.includes(w)))
1336
+ return true;
1337
+ return false;
1338
+ }
1339
+ /**
1340
+ * §40 C4:**容器严格等于裸鉴权类名** × **方法是授权决策或机制内部**。
1341
+ *
1342
+ * §39.8 记下的未收项:verdaccio `Auth.allow_publish` / `Auth.allow_access` /
1343
+ * `Auth.setLegacyAuthCacheEntry`。容器 `Auth` 不带 Guard/Strategy/AbilityFactory 后缀,
1344
+ * 方法也不是「机制动词 × 机制对象」 ⇒ §39 的结构化判据够不着。
1345
+ *
1346
+ * ⚠⚠ 两层约束**缺一不可**(宽变体实测,见 §40.5):
1347
+ * ① 容器必须**严格等于** Auth / Authorization / AccessControl / Permissions。
1348
+ * 放宽成 `Auth*` 会吃掉 `AuthController.deletePendingUserData` —— §39 的 V-D
1349
+ * 反向验证早就证明这类**不能**压(对凭据/待处理用户做增删改最需要鉴权)。
1350
+ * ② 方法必须是 `allow_*`(授权决策)或含机制名词(Cache/Token/JWT/AES/Session/
1351
+ * Middleware/Encrypt…),且**不得以受保护动词开头** —— 否则
1352
+ * `Auth.changePassword` / `Auth.invalidateToken` / `Auth.add_user` 会被压掉。
1353
+ */
1354
+ const AUTH_CLASS_EXACT = /^(?:Auth|Authorization|AccessControl|Permissions?)$/;
1355
+ const AUTH_MECHANISM_METHOD = /(?:Cache|Token|JWT|AES|Bearer|Session|Middleware|Encrypt|Decrypt|Payload|Signature)/i;
1356
+ const PROTECTED_VERB_PREFIX = /^(?:change|add|remove|delete|update|create|reset|invalidate|revoke|grant|deny|rotate)/i;
1357
+ function isAuthDecisionName(enclosingFuncName) {
1358
+ const dot = enclosingFuncName.lastIndexOf(".");
1359
+ if (dot <= 0)
1360
+ return false;
1361
+ if (!AUTH_CLASS_EXACT.test(enclosingFuncName.slice(0, dot)))
1362
+ return false;
1363
+ const method = enclosingFuncName.slice(dot + 1);
1364
+ if (PROTECTED_VERB_PREFIX.test(method))
1365
+ return false;
1366
+ return /^allow_/i.test(method) || AUTH_MECHANISM_METHOD.test(method);
1367
+ }
1368
+ /** 用户鉴权入口词表(login/register/logout/…)—— 原在两处重复定义,提成一份 */
1369
+ 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;
1370
+ /**
1371
+ * 该函数是否是「鉴权相关」函数 —— 是则跳过 authorization 类目规则
1372
+ * (对鉴权机制自己报「未鉴权」是自指谬误)。
1373
+ */
1374
+ function isAuthFunctionName(enclosingFuncName) {
1375
+ if (!enclosingFuncName)
1376
+ return false;
1377
+ const rawLower = enclosingFuncName.toLowerCase();
1378
+ if (AUTH_PATTERN.test(rawLower))
1379
+ return true;
1380
+ if (identifierParse(enclosingFuncName).some((w) => AUTH_PATTERN.test(w)))
1381
+ return true;
1382
+ if (isAuthMachineryName(enclosingFuncName))
1383
+ return true;
1384
+ if (isAuthDecisionName(enclosingFuncName))
1385
+ return true; // §40 C4
1386
+ // §40 C2:**函数自身**就是那个授权检查 ⇒ 对它报"缺检查"同样自指。
1387
+ // 例:verdaccio `canRemove` 报 "Authorization (Ownership Check)"。
1388
+ // 判据复用 §38 的三条(同源,不另起一份),只作用在**点号后的方法部分**。
1389
+ const dot = enclosingFuncName.lastIndexOf(".");
1390
+ const method = dot >= 0 ? enclosingFuncName.slice(dot + 1) : enclosingFuncName;
1391
+ if (exports.AUTHZ_PREDICATE_RE.test(method) ||
1392
+ exports.AUTHZ_CAN_HELPER_RE.test(method) ||
1393
+ exports.AUTHZ_ACCESS_CHECK_RE.test(method))
1394
+ return true;
1395
+ return false;
1396
+ }
1141
1397
  /**
1142
1398
  * Detect missing safeguards in function call sequences.
1143
1399
  * Uses identifier parsing to match compound names (registerNewUser → register).
1144
1400
  */
1145
- function detectSafeguardViolations(calls, enclosingFuncName, language, params, exposed) {
1401
+ /**
1402
+ * §44(2026-09-26)`paramGated` 的身份词表 —— 与旧的内联正则同源,抽出来是为了让
1403
+ * fixture 能 import 落地正则(R59),也让「主体 vs 被操作对象」这一刀有单一定义。
1404
+ *
1405
+ * ⚠ 这个词表**只能表达"参数名像身份"**,不能单独区分主体与被操作对象
1406
+ * (`token` 既可能是调用者凭证,也可能是被操作的 PAT)。
1407
+ * 区分靠**参数类型**(见 `classifyParamType`):主体是领域对象,被操作对象是标量。
1408
+ */
1409
+ exports.IDENTITY_PARAM_RE = /\b(token|session|user|auth|request|scope|cookie|credential|permission|role|identity)\b/i;
1410
+ /** 标量类型 —— 被操作对象几乎总是这些(id / email / token 都是 string)。 */
1411
+ const SCALAR_PARAM_TYPE_RE = /^(?:string|number|boolean|bigint|symbol|void|never|date)(?:\[\])?$/i;
1412
+ /** 类型解析不出来 —— 保守,不收紧(ts-morph 对 `any` / 解不出类型的参数会给出这些)。 */
1413
+ const UNKNOWN_PARAM_TYPE_RE = /^(?:any|unknown|object)(?:\[\])?$/i;
1414
+ const NULLISH_PARAM_TYPE_RE = /^(?:null|undefined)$/i;
1415
+ /**
1416
+ * 参数类型三分类。**"unknown" 一律按旧行为放行** —— 类型通道不可用不是压制的理由。
1417
+ * 联合类型(`User | null` / `AuthenticationType | null`)按"含任一领域类型即 typed"处理。
1418
+ */
1419
+ function classifyParamType(t) {
1420
+ const parts = (t || "")
1421
+ .split("|")
1422
+ .map((s) => s.trim())
1423
+ .filter(Boolean)
1424
+ .filter((s) => !NULLISH_PARAM_TYPE_RE.test(s));
1425
+ if (parts.length === 0)
1426
+ return "unknown";
1427
+ if (parts.every((s) => UNKNOWN_PARAM_TYPE_RE.test(s)))
1428
+ return "unknown";
1429
+ if (parts.some((s) => UNKNOWN_PARAM_TYPE_RE.test(s)))
1430
+ return "typed"; // 含 any 的联合 ⇒ 不敢判标量
1431
+ if (parts.every((s) => SCALAR_PARAM_TYPE_RE.test(s)))
1432
+ return "scalar";
1433
+ return "typed";
1434
+ }
1435
+ function detectSafeguardViolations(calls, enclosingFuncName, language, params, exposed, paramTypes) {
1146
1436
  const violations = [];
1147
1437
  // Build effective calls: raw names + identifier-parsed words.
1148
1438
  // Class-qualified names (Class.method) contribute only their METHOD name —
@@ -1156,10 +1446,8 @@ function detectSafeguardViolations(calls, enclosingFuncName, language, params, e
1156
1446
  }
1157
1447
  const effectiveCalls = [...new Set([...rawCalls, ...parsedWords])];
1158
1448
  // 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)));
1449
+ // §39:入口词表 ∪ 鉴权机制自身(自指排除),详见 isAuthFunctionName
1450
+ const isAuthFunction = isAuthFunctionName(enclosingFuncName);
1163
1451
  // Filter rules by language
1164
1452
  const activeRules = language
1165
1453
  ? SAFEGUARD_RULES.filter(r => !r.languages || r.languages.includes(language))
@@ -1197,11 +1485,32 @@ function detectSafeguardViolations(calls, enclosingFuncName, language, params, e
1197
1485
  // Surface gate (paramGated): only apply to functions that can plausibly
1198
1486
  // authenticate — routed by a web handler (exposed) or taking an
1199
1487
  // identity-ish parameter. Requires the caller to pass param names.
1488
+ //
1489
+ // §44(2026-09-26):光看参数名会把**被操作对象**当成**调用者身份** ——
1490
+ // `updateLastUsedForPAT(token: string)` 的 token 是要被更新的 PAT;
1491
+ // `addUserToTeam(teamID, userEmail, role)` 的 role 是要授予的角色。
1492
+ // 主体是**领域对象**(`user: AuthUser` / `user: User` / `adminUser: Admin`),
1493
+ // 被操作对象几乎总是**标量**(id / email / token 都是 string)⇒ 标量类型的参数
1494
+ // 不再充当身份证据。类型通道不可用时不收紧(退回旧行为,见 R57)。
1200
1495
  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));
1496
+ const hasIdentity = params.some((p, i) => {
1497
+ if (!exports.IDENTITY_PARAM_RE.test(p))
1498
+ return false;
1499
+ const t = paramTypes?.[i];
1500
+ // 调用方没传类型 / 该参数类型解析不出来(any、unknown、空)⇒ 保守放行
1501
+ if (t === undefined)
1502
+ return true;
1503
+ return classifyParamType(t) !== "scalar";
1504
+ });
1202
1505
  if (!hasIdentity && !exposed)
1203
1506
  continue;
1204
1507
  }
1508
+ // §47(2026-09-27)签名级会话证据:命中即视为「已认证」,与 calls 类 safeguard 并列。
1509
+ // 放在 `guards.find` 之前,语义上等价于命中了一个 safeguard。
1510
+ if (rule.paramTypeSafeguards && paramTypes) {
1511
+ if (paramTypes.some((t) => t && rule.paramTypeSafeguards.some((re) => re.test(t))))
1512
+ continue;
1513
+ }
1205
1514
  // Check if at least one safeguard matches
1206
1515
  const guards = (params && rule.strictSafeguards) ? rule.strictSafeguards : rule.safeguards;
1207
1516
  const matchedSafeguard = guards.find(s => {
@@ -1311,10 +1620,8 @@ function detectSafeguardViolationsV7(calls, enclosingFuncName, callerMap, funcCa
1311
1620
  for (const c of triggerEffectiveCalls)
1312
1621
  safeContext.add(c);
1313
1622
  // 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)));
1623
+ // §39:入口词表 ∪ 鉴权机制自身(自指排除),详见 isAuthFunctionName
1624
+ const isAuthFunction = isAuthFunctionName(enclosingFuncName);
1318
1625
  // Filter rules by language
1319
1626
  const activeRulesV7 = language
1320
1627
  ? 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.54",
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/**'",