progmune-runtime 3.7.13 → 3.7.15

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.
@@ -138,3 +138,106 @@ export class AppModule {}
138
138
  (0, vitest_1.expect)(issues.map((i) => i.type)).not.toContain("NESTJS_NO_AUTH");
139
139
  });
140
140
  });
141
+ (0, vitest_1.describe)("nestjs-detector 模块中间件覆盖(V1 修复回归)", () => {
142
+ (0, vitest_1.it)("NestModule configure/forRoutes 保护的 mutation 不报(Nest 5 惯用法)", () => {
143
+ write("tsconfig.json", TSCONFIG);
144
+ write("src/app.module.ts", `
145
+ import { Module, NestModule, MiddlewareConsumer, RequestMethod } from "@nestjs/common";
146
+ import { AuthMiddleware } from "./auth.middleware";
147
+ import { ApiController } from "./api.controller";
148
+
149
+ @Module({ controllers: [ApiController] })
150
+ export class AppModule implements NestModule {
151
+ configure(consumer: MiddlewareConsumer) {
152
+ consumer
153
+ .apply(AuthMiddleware)
154
+ .forRoutes(
155
+ { path: "api/transfer", method: RequestMethod.POST },
156
+ { path: "api/items/:id", method: RequestMethod.ALL },
157
+ );
158
+ }
159
+ }
160
+ `);
161
+ write("src/api.controller.ts", `
162
+ import { Controller, Post, Delete, Get } from "@nestjs/common";
163
+ @Controller("api")
164
+ export class ApiController {
165
+ @Post("transfer") transfer() { return {}; }
166
+ @Delete("items/:id") deleteItem() { return {}; }
167
+ @Post("public") publicCreate() { return {}; }
168
+ }
169
+ `);
170
+ write("src/auth.middleware.ts", `
171
+ import { NestMiddleware } from "@nestjs/common";
172
+ export class AuthMiddleware implements NestMiddleware { use() {} }
173
+ `);
174
+ const a = (0, nestjs_detector_1.analyzeNestJSProject)(dir);
175
+ const noAuth = a.issues.filter((i) => i.type === "NESTJS_NO_AUTH");
176
+ // transfer 与 items/:id 受 forRoutes 中间件保护 → 不报
177
+ (0, vitest_1.expect)(noAuth.map((i) => i.route)).not.toContain("POST api/transfer");
178
+ (0, vitest_1.expect)(noAuth.map((i) => i.route)).not.toContain("DELETE api/items/:id");
179
+ // 未覆盖的 public 写路由仍报(保留敏感性)
180
+ (0, vitest_1.expect)(noAuth.map((i) => i.route)).toContain("POST api/public");
181
+ });
182
+ (0, vitest_1.it)("摘除 forRoutes 中间件覆盖后 mutation 重新被报(无感修复回归)", () => {
183
+ write("tsconfig.json", TSCONFIG);
184
+ write("src/app.module.ts", `
185
+ import { Module, NestModule, MiddlewareConsumer, RequestMethod } from "@nestjs/common";
186
+ import { ApiController } from "./api.controller";
187
+ @Module({ controllers: [ApiController] })
188
+ export class AppModule implements NestModule {
189
+ configure(consumer: MiddlewareConsumer) {
190
+ consumer.apply(AuthMiddleware).forRoutes(); // 覆盖被摘空
191
+ }
192
+ }
193
+ `);
194
+ write("src/api.controller.ts", `
195
+ import { Controller, Post } from "@nestjs/common";
196
+ @Controller("api")
197
+ export class ApiController {
198
+ @Post("transfer") transfer() { return {}; }
199
+ }
200
+ `);
201
+ write("src/auth.middleware.ts", `
202
+ import { NestMiddleware } from "@nestjs/common";
203
+ export class AuthMiddleware implements NestMiddleware { use() {} }
204
+ `);
205
+ const a = (0, nestjs_detector_1.analyzeNestJSProject)(dir);
206
+ (0, vitest_1.expect)(a.issues.some((i) => i.type === "NESTJS_NO_AUTH" && i.route === "POST api/transfer")).toBe(true);
207
+ });
208
+ });
209
+ (0, vitest_1.describe)("nestjs-detector register 集合豁免(语义层)", () => {
210
+ function writeUserApp() {
211
+ write("tsconfig.json", TSCONFIG);
212
+ write("src/user.controller.ts", `
213
+ import { Controller, Post, Get, Delete } from "@nestjs/common";
214
+ @Controller()
215
+ export class UserController {
216
+ @Post("users/login") login() { return {}; }
217
+ @Post("users") register() { return {}; } // 公开注册
218
+ @Delete("users/:slug") deleteUser() { return {}; } // 真实无保护
219
+ }
220
+ `);
221
+ }
222
+ (0, vitest_1.it)("有 users/login 姊妹佐证:POST users(公开注册)不报", () => {
223
+ writeUserApp();
224
+ const a = (0, nestjs_detector_1.analyzeNestJSProject)(dir);
225
+ const noAuth = a.issues.filter((i) => i.type === "NESTJS_NO_AUTH");
226
+ (0, vitest_1.expect)(noAuth.map((i) => i.route)).not.toContain("POST /users");
227
+ (0, vitest_1.expect)(noAuth.map((i) => i.route)).not.toContain("POST /users/login");
228
+ // 真实无保护的 DELETE 仍报(豁免不误伤)
229
+ (0, vitest_1.expect)(noAuth.map((i) => i.route)).toContain("DELETE /users/:slug");
230
+ });
231
+ (0, vitest_1.it)("无姊妹佐证:POST users 仍报(管理员建用户不豁免)", () => {
232
+ write("tsconfig.json", TSCONFIG);
233
+ write("src/admin.controller.ts", `
234
+ import { Controller, Post } from "@nestjs/common";
235
+ @Controller()
236
+ export class AdminController {
237
+ @Post("users") adminCreateUser() { return {}; }
238
+ }
239
+ `);
240
+ const a = (0, nestjs_detector_1.analyzeNestJSProject)(dir);
241
+ (0, vitest_1.expect)(a.issues.some((i) => i.type === "NESTJS_NO_AUTH" && i.route === "POST /users")).toBe(true);
242
+ });
243
+ });
@@ -66,7 +66,10 @@ const AUTH_ENTRY_WORDS = [
66
66
  "login", "signin", "sign_in", "regist", "signup", "sign_up",
67
67
  "token", "auth", "health",
68
68
  ];
69
- const AUTH_CALL_RE = /\b(getServerSession|requireAuth|requireUser|verifyToken|verifyAuth|isAuthenticated|checkAuth|getToken|withAuth|authSession|authenticate)\s*\(/;
69
+ // 路由级认证信号:会话式(getServerSession/next-auth v5 auth()/clerk
70
+ // currentUser)+ webhook 载荷签名校验(Stripe constructEvent 等——
71
+ // V5 真实语料证明 webhook 端点的标准保护是签名校验而非会话)
72
+ const AUTH_CALL_RE = /\b(getServerSession|requireAuth|requireUser|verifyToken|verifyAuth|isAuthenticated|checkAuth|getToken|withAuth|authSession|authenticate|auth|currentUser)\s*\(|\b(constructEvent|verifyWebhook|verifySignature|verifyWebhookSignature|validateWebhook|validateSignature)\s*\(/;
70
73
  const AUTH_MIDDLEWARE_RE = /\b(getServerSession|requireAuth|verifyToken|getToken|withAuth|next-auth|authorization|authenticate)\b/;
71
74
  function isAuthEntryFile(relFile) {
72
75
  const lower = relFile.toLowerCase();
@@ -110,4 +110,39 @@ export default withAuth(function middleware(req) {});
110
110
  (0, vitest_1.expect)(hasNext).toBe(false);
111
111
  (0, vitest_1.expect)(issues).toHaveLength(0);
112
112
  });
113
+ (0, vitest_1.it)("V5 修复回归:Stripe webhook 签名校验(constructEvent)视为端点认证——不报", () => {
114
+ writeRoute("app/api/webhooks/stripe/route.ts", `
115
+ import { stripe } from "@/lib/stripe";
116
+ export async function POST(req: Request) {
117
+ const body = await req.text();
118
+ const signature = req.headers.get("stripe-signature") as string;
119
+ const event = stripe.webhooks.constructEvent(body, signature, process.env.SECRET!);
120
+ return Response.json({ received: true });
121
+ }
122
+ `);
123
+ const { issues } = (0, nextjs_detector_1.analyzeNextApp)(dir);
124
+ (0, vitest_1.expect)(issues).toHaveLength(0);
125
+ });
126
+ (0, vitest_1.it)("V5 修复回归:webhook 无签名校验仍报(保留对真缺失认证的敏感性)", () => {
127
+ writeRoute("app/api/webhooks/stripe/route.ts", `
128
+ export async function POST(req: Request) {
129
+ const body = await req.text();
130
+ return Response.json({ received: true });
131
+ }
132
+ `);
133
+ const { issues } = (0, nextjs_detector_1.analyzeNextApp)(dir);
134
+ (0, vitest_1.expect)(issues.map((i) => i.rule)).toContain("NEXT_ROUTE_NO_AUTH");
135
+ });
136
+ (0, vitest_1.it)("V5 修复回归:next-auth v5 / clerk 裸 auth() 调用视为认证——不报", () => {
137
+ writeRoute("app/api/transfer/route.ts", `
138
+ import { auth } from "@/auth";
139
+ export async function POST(req: Request) {
140
+ const session = await auth();
141
+ if (!session?.user) return new Response(null, { status: 403 });
142
+ return Response.json({ ok: true });
143
+ }
144
+ `);
145
+ const { issues } = (0, nextjs_detector_1.analyzeNextApp)(dir);
146
+ (0, vitest_1.expect)(issues).toHaveLength(0);
147
+ });
113
148
  });
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ /**
3
+ * route-window.ts — 路由调用窗口共享工具(Koa/Gin/Fiber 代码串检测器共用)
4
+ *
5
+ * 背景:初代实现用「路径串后固定 300 字符窗口」收集认证中间件名 →
6
+ * 跨路由串扰(bleed):后面路由的 auth 名污染前面公开路由的保护判定
7
+ * (V3 Koa / V7 Gin / V8 Fiber 同款缺陷)。
8
+ *
9
+ * 修复思路:
10
+ * 1. routeCallWindow —— 窗口 = 本次路由调用边界内(括号深度感知到闭合
11
+ * `)`),不跨路由、容忍内联 handler 的多层括号
12
+ * 2. middlewareNamesFromWindow —— koa-router/gin/fiber 语义一致:
13
+ * `(path, ...middleware, handler)`,最后一个纯函数引用参数是
14
+ * handler(如 ctrl.login / UsersLogin),排除——handler 名含 auth
15
+ * 词不再被误判为认证中间件
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.routeCallWindow = routeCallWindow;
19
+ exports.middlewareNamesFromWindow = middlewareNamesFromWindow;
20
+ exports.normPath = normPath;
21
+ exports.collectRegisterRoots = collectRegisterRoots;
22
+ exports.isRegisterRoot = isRegisterRoot;
23
+ /**
24
+ * 自路由调用起点(路径串刚结束处)扫描到本次调用的闭合括号。
25
+ * @param code 源码
26
+ * @param from 路径串结束位置(调用括号深度已为 1——方法名的 `(` 已消费)
27
+ * @returns 窗口内容(含闭合 `)`)
28
+ */
29
+ function routeCallWindow(code, from) {
30
+ let depth = 1;
31
+ let end = from;
32
+ let quote = null;
33
+ const MAX = 4000;
34
+ while (end < code.length && end - from < MAX) {
35
+ const ch = code[end];
36
+ if (quote) {
37
+ if (ch === quote && code[end - 1] !== "\\")
38
+ quote = null;
39
+ }
40
+ else if (ch === '"' || ch === "'" || ch === "`") {
41
+ quote = ch;
42
+ }
43
+ else if (ch === "(") {
44
+ depth++;
45
+ }
46
+ else if (ch === ")") {
47
+ depth--;
48
+ if (depth === 0)
49
+ break;
50
+ }
51
+ end++;
52
+ }
53
+ return code.slice(from, end + 1);
54
+ }
55
+ /** 纯函数引用:auth / ctrl.login / users.AuthMiddleware */
56
+ function isPlainFnRef(s) {
57
+ return /^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*)*$/.test(s.trim());
58
+ }
59
+ /**
60
+ * 从路由调用参数窗口提取中间件候选名。
61
+ * 顶层逗号切分(深度感知)→ 末参若为纯函数引用则视为 handler 排除 →
62
+ * 其余参数内的标识符即中间件候选。
63
+ */
64
+ function middlewareNamesFromWindow(window) {
65
+ const names = [];
66
+ const body = window.replace(/\)\s*$/, "");
67
+ const parts = [];
68
+ let cur = "";
69
+ let d = 0;
70
+ for (const ch of body) {
71
+ if (ch === "(" || ch === "[" || ch === "{")
72
+ d++;
73
+ else if (ch === ")" || ch === "]" || ch === "}")
74
+ d--;
75
+ if (ch === "," && d === 0) {
76
+ parts.push(cur);
77
+ cur = "";
78
+ }
79
+ else {
80
+ cur += ch;
81
+ }
82
+ }
83
+ if (cur.trim().length > 0)
84
+ parts.push(cur);
85
+ const mwParts = isPlainFnRef(parts[parts.length - 1] ?? "")
86
+ ? parts.slice(0, -1)
87
+ : parts;
88
+ for (const p of mwParts) {
89
+ const found = p.match(/[A-Za-z_$][\w$]*/g) || [];
90
+ names.push(...found);
91
+ }
92
+ return names;
93
+ }
94
+ // ── register 集合豁免(语义层,Koa/Gin/Fiber/NestJS 共用)──
95
+ /** 账户集合根判定词:路径以这些后缀结尾的路由是账户入口(login 等) */
96
+ const ACCOUNT_ENTRY_SUFFIXES = [
97
+ "/login", "/signin", "/sign_in", "/register", "/signup", "/sign_up",
98
+ ];
99
+ /** 规范化路径:去首尾斜杠 */
100
+ function normPath(p) {
101
+ return p.replace(/^\/+|\/+$/g, "");
102
+ }
103
+ /**
104
+ * 收集「账户集合根」:对每条路径,若以账户入口后缀结尾(如 /users/login),
105
+ * 剥掉后缀得集合根(/users)。该集合的无认证 POST = 公开注册(豁免)。
106
+ * 佐证式豁免——无 login/register 姊妹的写集合不豁免(管理员建用户等仍查)。
107
+ */
108
+ function collectRegisterRoots(paths) {
109
+ const roots = new Set();
110
+ for (const p of paths) {
111
+ const lower = p.toLowerCase();
112
+ for (const suffix of ACCOUNT_ENTRY_SUFFIXES) {
113
+ if (lower.endsWith(suffix)) {
114
+ roots.add(normPath(p.slice(0, -suffix.length)));
115
+ break;
116
+ }
117
+ }
118
+ }
119
+ return roots;
120
+ }
121
+ /** routePath 是否命中账户集合根(规范化比较) */
122
+ function isRegisterRoot(routePath, roots) {
123
+ return roots.has(normPath(routePath));
124
+ }
@@ -26,7 +26,9 @@ exports.extractRouterNames = extractRouterNames;
26
26
  exports.extractProcedures = extractProcedures;
27
27
  exports.analyzeTRPCFile = analyzeTRPCFile;
28
28
  // ── Detection Patterns ──
29
- const PROCEDURE_TYPE_PATTERN = /\b(publicProcedure|protectedProcedure|adminProcedure)\b/g;
29
+ // 注意:detect 用途不带 /g——带 /g 的 test() 会跨文件泄漏 lastIndex,
30
+ // 导致逐文件扫描结果随顺序漂移(实测 4/19 vs 7/19)
31
+ const PROCEDURE_TYPE_PATTERN = /\b(publicProcedure|protectedProcedure|adminProcedure)\b/;
30
32
  const DB_WRITE_PATTERN = /\b(db\.(insert|update|delete|create|upsert|execute)|prisma\.\w+\.(create|update|delete|upsert|createMany|updateMany|deleteMany)|drizzle\.(insert|update|delete)|\.(insert|update|delete|create|upsert)\s*\()/i;
31
33
  /**
32
34
  * Detect whether this file contains tRPC definitions.
@@ -57,30 +59,94 @@ function extractRouterNames(code) {
57
59
  */
58
60
  function extractProcedures(code) {
59
61
  const procedures = [];
60
- // Match: name: <procedureType>.input(...).<kind>( or name: <procedureType>.<kind>(
61
- const procRe = /([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(publicProcedure|protectedProcedure|adminProcedure)((?:\.\w+\s*\([^()]*\))*)\.(query|mutation)\s*\(/g;
62
+ // 过程起点:name: <procedureType>(不含链)——v11 惯用法 t.procedure
63
+ // (V4 遗留缺口:只认 XxxProcedure 命名包装,内联 t.procedure 不可见)
64
+ const procStartRe = /([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(t\s*\.\s*procedure|publicProcedure|protectedProcedure|adminProcedure)\b/g;
62
65
  let m;
63
- while ((m = procRe.exec(code)) !== null) {
66
+ while ((m = procStartRe.exec(code)) !== null) {
64
67
  const name = m[1];
65
- const procType = m[2].replace(/Procedure$/, "");
66
- const chain = m[3] || "";
67
- const kind = m[4];
68
- const hasInputSchema = /\.input\s*\(/.test(chain);
69
- // Extract body until matching closing paren of the query/mutation call.
70
- // Heuristic: scan forward for the closing paren matching the one after
71
- // the .query( / .mutation( token.
72
- const openIdx = procRe.lastIndex - 1; // index of '(' after query/mutation
68
+ // t.procedure = v11 基础构造器(无包装 → 默认公开语义);命名包装去掉后缀
69
+ const procType = (m[2].includes(".") ? "public" : m[2].replace(/Procedure$/, ""));
70
+ // ── 链扫描(括号感知)──
71
+ // procedure 类型后逐个解析 .method(balancedArgs),容忍嵌套括号与
72
+ // 多行(.input(z.object({...})) 等标准形态),直至 .query(/.mutation(
73
+ // 或链中断。旧实现用 (?:\.\w+\([^()]*\))* 不跨嵌套括号 标准
74
+ // zod input 链整体失明(V4 缺陷)。
75
+ let pos = procStartRe.lastIndex;
76
+ let hasInputSchema = false;
77
+ let kind = null;
78
+ let kindOpenIdx = -1; // .query( 或 .mutation( 的 '(' 下标
79
+ const skipWs = () => {
80
+ while (pos < code.length && /\s/.test(code[pos]))
81
+ pos++;
82
+ };
83
+ const consumeBalanced = (open, close) => {
84
+ let depth = 1;
85
+ let quote = null;
86
+ pos++; // 跳过 open
87
+ while (pos < code.length && depth > 0) {
88
+ const ch = code[pos];
89
+ if (quote) {
90
+ if (ch === quote && code[pos - 1] !== "\\")
91
+ quote = null;
92
+ }
93
+ else if (ch === '"' || ch === "'" || ch === "`") {
94
+ quote = ch;
95
+ }
96
+ else if (ch === open) {
97
+ depth++;
98
+ }
99
+ else if (ch === close) {
100
+ depth--;
101
+ }
102
+ pos++;
103
+ }
104
+ };
105
+ for (let step = 0; step < 100; step++) {
106
+ skipWs();
107
+ if (code[pos] !== ".")
108
+ break;
109
+ pos++;
110
+ const methStart = pos;
111
+ while (pos < code.length && /[A-Za-z0-9_$]/.test(code[pos]))
112
+ pos++;
113
+ const method = code.slice(methStart, pos);
114
+ skipWs();
115
+ if (code[pos] !== "(")
116
+ break;
117
+ if (method === "query" || method === "mutation") {
118
+ kind = method;
119
+ kindOpenIdx = pos; // '(' 位置
120
+ break;
121
+ }
122
+ if (method === "input")
123
+ hasInputSchema = true;
124
+ consumeBalanced("(", ")");
125
+ }
126
+ if (kind === null || kindOpenIdx < 0)
127
+ continue; // 非完整过程定义
128
+ // ── body:自 kind 的 '(' 后到匹配闭合括号(字符串感知)──
73
129
  let depth = 1;
74
- let closeIdx = openIdx + 1;
130
+ let closeIdx = kindOpenIdx + 1;
131
+ let quote = null;
75
132
  while (closeIdx < code.length && depth > 0) {
76
133
  const ch = code[closeIdx];
77
- if (ch === "(")
134
+ if (quote) {
135
+ if (ch === quote && code[closeIdx - 1] !== "\\")
136
+ quote = null;
137
+ }
138
+ else if (ch === '"' || ch === "'" || ch === "`") {
139
+ quote = ch;
140
+ }
141
+ else if (ch === "(") {
78
142
  depth++;
79
- else if (ch === ")")
143
+ }
144
+ else if (ch === ")") {
80
145
  depth--;
146
+ }
81
147
  closeIdx++;
82
148
  }
83
- const body = code.slice(openIdx + 1, Math.min(closeIdx - 1, openIdx + 2000));
149
+ const body = code.slice(kindOpenIdx + 1, Math.min(closeIdx - 1, kindOpenIdx + 2000));
84
150
  const usesInputInBody = /input\s*\./.test(body) || /\binput\b/.test(body);
85
151
  const doesDbWrite = DB_WRITE_PATTERN.test(body);
86
152
  procedures.push({
@@ -0,0 +1,177 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ /**
37
+ * trpc-detector.test.ts — tRPC 检测器回归(纯函数,无文件 I/O)
38
+ *
39
+ * 覆盖 V4 真实语料(netflx-web)暴露的两项缺陷:
40
+ * 1. 链匹配正则不跨嵌套括号 → 标准 .input(z.object({...})) 过程失明
41
+ * 2. PROCEDURE_TYPE_PATTERN /g lastIndex 泄漏 → 逐文件扫描漂移
42
+ */
43
+ const vitest_1 = require("vitest");
44
+ const trpc_detector_1 = require("./trpc-detector");
45
+ const fs = __importStar(require("fs"));
46
+ const os = __importStar(require("os"));
47
+ const path = __importStar(require("path"));
48
+ // ── 修复 1:标准 .input 链(嵌套括号/多行)必须可见 ──
49
+ const ROUTER_WITH_INPUT = `
50
+ const t = initTRPC.context<{ db: Db }>().create();
51
+ export const postRouter = t.router({
52
+ addComment: protectedProcedure
53
+ .input(
54
+ z.object({
55
+ articleId: z.string(),
56
+ body: z.string().min(1),
57
+ })
58
+ )
59
+ .mutation(async ({ ctx, input }) => {
60
+ await ctx.db.comment.create({ data: { articleId: input.articleId } });
61
+ }),
62
+ list: publicProcedure.query(async ({ ctx }) => ctx.db.comment.findMany()),
63
+ });`;
64
+ const ROUTER_WITH_BARE_MUTATION = `
65
+ export const appRouter = t.router({
66
+ deleteAll: publicProcedure.mutation(async ({ ctx, input }) => {
67
+ await ctx.prisma.post.deleteMany();
68
+ }),
69
+ });`;
70
+ (0, vitest_1.describe)("trpc extractProcedures — 括号感知链", () => {
71
+ (0, vitest_1.it)("标准多行 .input(z.object({...})) mutation 可见且有 input schema(V4 缺陷回归)", () => {
72
+ const procs = (0, trpc_detector_1.extractProcedures)(ROUTER_WITH_INPUT);
73
+ const add = procs.find((p) => p.name === "addComment");
74
+ (0, vitest_1.expect)(add).toBeDefined();
75
+ (0, vitest_1.expect)(add.kind).toBe("mutation");
76
+ (0, vitest_1.expect)(add.procedureType).toBe("protected");
77
+ (0, vitest_1.expect)(add.hasInputSchema).toBe(true);
78
+ // 无 schema 的 query 也照常可见
79
+ (0, vitest_1.expect)(procs.some((p) => p.name === "list" && p.hasInputSchema === false)).toBe(true);
80
+ });
81
+ (0, vitest_1.it)("单行 .input(z.string()) 链可见", () => {
82
+ const procs = (0, trpc_detector_1.extractProcedures)(`
83
+ export const r = t.router({
84
+ getOne: protectedProcedure.input(z.string()).query(async ({ ctx, input }) => {
85
+ return ctx.db.get(input);
86
+ }),
87
+ });`);
88
+ const p = procs.find((x) => x.name === "getOne");
89
+ (0, vitest_1.expect)(p).toBeDefined();
90
+ (0, vitest_1.expect)(p.hasInputSchema).toBe(true);
91
+ });
92
+ (0, vitest_1.it)("裸链 mutation(无 input)仍可见并可触发规则", () => {
93
+ const procs = (0, trpc_detector_1.extractProcedures)(ROUTER_WITH_BARE_MUTATION);
94
+ const del = procs.find((p) => p.name === "deleteAll");
95
+ (0, vitest_1.expect)(del).toBeDefined();
96
+ (0, vitest_1.expect)(del.kind).toBe("mutation");
97
+ (0, vitest_1.expect)(del.hasInputSchema).toBe(false);
98
+ (0, vitest_1.expect)(del.doesDbWrite).toBe(true);
99
+ });
100
+ (0, vitest_1.it)("完整分析:合规 router 0 issues,裸 public mutation 报 TRPC_PUBLIC_MUTATION", () => {
101
+ const tmp = path.join(os.tmpdir(), "trpc-good-router.ts");
102
+ fs.writeFileSync(tmp, ROUTER_WITH_INPUT);
103
+ try {
104
+ const good = (0, trpc_detector_1.analyzeTRPCFile)(tmp);
105
+ (0, vitest_1.expect)(good.issues).toHaveLength(0);
106
+ (0, vitest_1.expect)(good.procedures.length).toBe(2);
107
+ }
108
+ finally {
109
+ fs.unlinkSync(tmp);
110
+ }
111
+ const tmp2 = path.join(os.tmpdir(), "trpc-bad-router.ts");
112
+ fs.writeFileSync(tmp2, ROUTER_WITH_BARE_MUTATION);
113
+ try {
114
+ const bad = (0, trpc_detector_1.analyzeTRPCFile)(tmp2);
115
+ (0, vitest_1.expect)(bad.issues.map((i) => i.rule)).toContain("TRPC_PUBLIC_MUTATION");
116
+ (0, vitest_1.expect)(bad.issues.map((i) => i.rule)).toContain("TRPC_MUTATION_WITHOUT_INPUT_SCHEMA");
117
+ }
118
+ finally {
119
+ fs.unlinkSync(tmp2);
120
+ }
121
+ });
122
+ });
123
+ // ── 修复 2:lastIndex 泄漏回归 ──
124
+ (0, vitest_1.describe)("detectTRPCApp — 无 /g lastIndex 泄漏", () => {
125
+ (0, vitest_1.it)("连续多次调用结果稳定(旧 /g 实现会漂移)", () => {
126
+ const trpcCode = `const t = initTRPC.create(); export const r = t.router({ a: publicProcedure.query(() => 1) });`;
127
+ const plainCode = `export const sum = (a: number, b: number) => a + b;`;
128
+ // 交替调用多次:泄漏时第二次起结果不稳定
129
+ const results = [];
130
+ for (let i = 0; i < 6; i++) {
131
+ results.push((0, trpc_detector_1.detectTRPCApp)(trpcCode)); // 应为 true
132
+ results.push((0, trpc_detector_1.detectTRPCApp)(plainCode)); // 应为 false
133
+ }
134
+ (0, vitest_1.expect)(results.filter(Boolean)).toHaveLength(6); // 恰好 6 个 true
135
+ (0, vitest_1.expect)(results).toEqual([
136
+ true, false, true, false, true, false, true, false, true, false, true, false,
137
+ ]);
138
+ });
139
+ });
140
+ // ── tRPC v11:内联 t.procedure 形态(V4 遗留缺口)──
141
+ (0, vitest_1.describe)("trpc v11 t.procedure 支持", () => {
142
+ (0, vitest_1.it)("t.procedure.input(z.object).mutation 可见且视为公开(默认语义)", () => {
143
+ const procs = (0, trpc_detector_1.extractProcedures)(`
144
+ import { initTRPC } from "@trpc/server";
145
+ const t = initTRPC.create();
146
+ export const r = t.router({
147
+ ping: t.procedure
148
+ .input(z.object({ msg: z.string() }))
149
+ .mutation(async ({ ctx, input }) => {
150
+ await ctx.prisma.log.create({ data: { msg: input.msg } });
151
+ }),
152
+ list: t.procedure.query(async () => []),
153
+ });`);
154
+ const ping = procs.find((p) => p.name === "ping");
155
+ (0, vitest_1.expect)(ping).toBeDefined();
156
+ (0, vitest_1.expect)(ping.kind).toBe("mutation");
157
+ (0, vitest_1.expect)(ping.procedureType).toBe("public");
158
+ (0, vitest_1.expect)(ping.hasInputSchema).toBe(true);
159
+ (0, vitest_1.expect)(procs.some((p) => p.name === "list")).toBe(true);
160
+ });
161
+ (0, vitest_1.it)("裸 t.procedure mutation(无 input)触发规则(敏感性与命名包装一致)", () => {
162
+ const procs = (0, trpc_detector_1.extractProcedures)(`
163
+ export const r = t.router({
164
+ nuke: t.procedure.mutation(async ({ ctx }) => {
165
+ await ctx.prisma.post.deleteMany();
166
+ }),
167
+ });`);
168
+ const nuke = procs.find((p) => p.name === "nuke");
169
+ (0, vitest_1.expect)(nuke).toBeDefined();
170
+ (0, vitest_1.expect)(nuke.hasInputSchema).toBe(false);
171
+ (0, vitest_1.expect)(nuke.doesDbWrite).toBe(true);
172
+ });
173
+ (0, vitest_1.it)("netflx v10 命名包装形态不受影响(19/19 保持)", () => {
174
+ const procs = (0, trpc_detector_1.extractProcedures)(ROUTER_WITH_INPUT);
175
+ (0, vitest_1.expect)(procs.length).toBe(2);
176
+ });
177
+ });
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.13";
23
+ exports.RUNTIME_VERSION = "3.7.15";
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.13",
3
+ "version": "3.7.15",
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/",
@@ -114,4 +114,4 @@
114
114
  "tsx": "^4.22.4",
115
115
  "vitest": "^3.2.6"
116
116
  }
117
- }
117
+ }