progmune-runtime 3.7.14 → 3.7.16

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.
@@ -53,7 +53,14 @@ var __importStar = (this && this.__importStar) || (function () {
53
53
  Object.defineProperty(exports, "__esModule", { value: true });
54
54
  exports.analyzeFiberApp = analyzeFiberApp;
55
55
  exports.analyzeFiberFile = analyzeFiberFile;
56
+ exports.fiberFuncStarts = fiberFuncStarts;
57
+ exports.fiberEnclosingFunc = fiberEnclosingFunc;
58
+ exports.fiberProtectedRegisterFns = fiberProtectedRegisterFns;
59
+ exports.analyzeFiberProject = analyzeFiberProject;
60
+ exports.goFuncsOf = goFuncsOf;
61
+ exports.fiberProjectProtectedFns = fiberProjectProtectedFns;
56
62
  const fs = __importStar(require("fs"));
63
+ const route_window_1 = require("./route-window");
57
64
  const MUTATION_METHODS = new Set(["post", "put", "patch", "delete"]);
58
65
  const AUTH_ENTRY_WORDS = [
59
66
  "login", "signin", "sign_in", "regist", "signup", "sign_up",
@@ -81,19 +88,22 @@ function analyzeFiberApp(code) {
81
88
  return { hasFiber: false, routes, authMiddleware, issues };
82
89
  }
83
90
  // 全局/组级认证中间件:app.Use(authMW) / group.Use(authMW)
84
- const useRe = /\.Use\s*\(\s*([A-Za-z_][\w]*)/g;
91
+ // 捕获支持点限定成员(jwtware.New 前的一般为 middleware.Protected 等)
92
+ const useRe = /\.Use\s*\(\s*([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)/g;
85
93
  let m;
86
94
  while ((m = useRe.exec(code)) !== null) {
87
95
  if (isAuthFnName(m[1]))
88
96
  authMiddleware.push(m[1]);
89
97
  }
90
98
  // 路由注册:app.Post("/x", mw1, mw2, handler)——Go 方法名大写(Post 惯例)
91
- const routeRe = /\.(get|post|put|patch|delete)\s*\(\s*"([^"]+)"/gi;
99
+ // 空路径 "..." 允许
100
+ const routeRe = /\.(get|post|put|patch|delete)\s*\(\s*"([^"]*)"(\s*,\s*|\s*\))/gi;
92
101
  while ((m = routeRe.exec(code)) !== null) {
93
102
  const method = m[1].toLowerCase();
94
103
  const pathName = m[2];
95
- const window = code.slice(m.index + m[0].length, m.index + m[0].length + 300);
96
- const mwNames = window.match(/[A-Za-z_][\w]*/g) || [];
104
+ // 认证窗口 = 本次调用边界内(括号感知),不跨路由(V8 缺陷修复)
105
+ const window = (0, route_window_1.routeCallWindow)(code, m.index + m[0].length);
106
+ const mwNames = (0, route_window_1.middlewareNamesFromWindow)(window);
97
107
  const hasAuthMw = mwNames.some((name) => isAuthFnName(name));
98
108
  routes.push({
99
109
  method,
@@ -101,15 +111,21 @@ function analyzeFiberApp(code) {
101
111
  protected: hasAuthMw,
102
112
  line: code.slice(0, m.index).split("\n").length,
103
113
  });
104
- if (MUTATION_METHODS.has(method) && !hasAuthMw
105
- && authMiddleware.length === 0 && !isAuthEntryPath(pathName)) {
114
+ }
115
+ // register 集合豁免(语义层,同 Koa/Gin)
116
+ const registerRoots = (0, route_window_1.collectRegisterRoots)(routes.map((r) => r.path));
117
+ for (const r of routes) {
118
+ if (MUTATION_METHODS.has(r.method) && !r.protected
119
+ && authMiddleware.length === 0
120
+ && !isAuthEntryPath(r.path)
121
+ && !(r.method === "post" && (0, route_window_1.isRegisterRoot)(r.path, registerRoots))) {
106
122
  issues.push({
107
123
  severity: "medium",
108
124
  rule: "FIBER_ROUTE_NO_AUTH",
109
- message: `Route ${method.toUpperCase()} ${pathName} has no auth middleware ` +
125
+ message: `Route ${r.method.toUpperCase()} ${r.path} has no auth middleware ` +
110
126
  `and no auth Use middleware — any caller can reach it.`,
111
- route: `${method.toUpperCase()} ${pathName}`,
112
- line: code.slice(0, m.index).split("\n").length,
127
+ route: `${r.method.toUpperCase()} ${r.path}`,
128
+ line: r.line,
113
129
  });
114
130
  }
115
131
  }
@@ -123,3 +139,244 @@ function analyzeFiberFile(filePath) {
123
139
  return null;
124
140
  return analyzeFiberApp(code);
125
141
  }
142
+ /** 顶层函数头(行号 1-based) */
143
+ function fiberFuncStarts(text) {
144
+ const out = [];
145
+ const re = /^func\s+([A-Za-z_]\w*)\s*\(/gm;
146
+ let m;
147
+ while ((m = re.exec(text)) !== null) {
148
+ out.push({ name: m[1], line: text.slice(0, m.index).split("\n").length });
149
+ }
150
+ return out;
151
+ }
152
+ /** routeLine 所在顶层函数名 */
153
+ function fiberEnclosingFunc(text, routeLine) {
154
+ let name = null;
155
+ for (const f of fiberFuncStarts(text)) {
156
+ if (f.line <= routeLine)
157
+ name = f.name;
158
+ else
159
+ break;
160
+ }
161
+ return name;
162
+ }
163
+ /** bootstrap 相位推导:认证 Use 之后调用的 Register fn(组编号 1,2|3,4|5,6|7,8) */
164
+ function fiberProtectedRegisterFns(bootstrapText) {
165
+ const state = new Map();
166
+ const protectedFns = new Map();
167
+ const re = /(\w+)\s*(?::=|=)\s*fiber\.New\s*\(|(\w+)\.Use\s*\(\s*([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)|(\w+)\s*(?::=|=)\s*(?:[\w.]*\.)?([A-Za-z_]\w*)\.Group\s*\(|(?:[\w.]*\.)?([A-Z][A-Za-z0-9_]*)\s*\(\s*(?:[\w.]*\.)?([A-Za-z_]\w*)\.Group\s*\(/g;
168
+ let m;
169
+ while ((m = re.exec(bootstrapText)) !== null) {
170
+ if (m[1] !== undefined) {
171
+ state.set(m[1], false);
172
+ }
173
+ else if (m[2] !== undefined) {
174
+ if (isAuthFnName(m[3]))
175
+ state.set(m[2], true);
176
+ }
177
+ else if (m[4] !== undefined) {
178
+ state.set(m[4], !!state.get(m[5]));
179
+ }
180
+ else if (m[6] !== undefined) {
181
+ if (state.get(m[7]))
182
+ protectedFns.set(m[6], true);
183
+ }
184
+ }
185
+ return protectedFns;
186
+ }
187
+ /** 项目级分析:跨文件组认证传播(同 gin analyzeGinProject) */
188
+ function analyzeFiberProject(projectRoot) {
189
+ const files = [];
190
+ const walk = (d) => {
191
+ for (const e of fs.readdirSync(d, { withFileTypes: true })) {
192
+ if (e.isDirectory()) {
193
+ if (["vendor", "node_modules", ".git"].includes(e.name))
194
+ continue;
195
+ walk(`${d}/${e.name}`);
196
+ }
197
+ else if (e.name.endsWith(".go") && !e.name.endsWith("_test.go")) {
198
+ const fp = `${d}/${e.name}`;
199
+ try {
200
+ files.push({ file: fp, text: fs.readFileSync(fp, "utf-8"), a: analyzeFiberFile(fp) });
201
+ }
202
+ catch { /* skip */ }
203
+ }
204
+ }
205
+ };
206
+ if (fs.existsSync(projectRoot))
207
+ walk(projectRoot.replace(/\/$/, ""));
208
+ // 多层 Register 链传播(journalist 式 main→api.Register(Group+Use)→…)
209
+ const protectedFns = fiberProjectProtectedFns(files);
210
+ const issues = [];
211
+ for (const { file, text, a } of files) {
212
+ if (!a || a.issues.length === 0)
213
+ continue;
214
+ const pkgM = text.match(/^package\s+(\w+)/m);
215
+ const filePkg = pkgM ? pkgM[1] : "";
216
+ for (const issue of a.issues) {
217
+ const fn = issue.line ? fiberEnclosingFunc(text, issue.line) : null;
218
+ if (fn && protectedFns.get(`${filePkg}:${fn}`))
219
+ continue;
220
+ issues.push({ ...issue });
221
+ }
222
+ }
223
+ return {
224
+ filesScanned: files.length,
225
+ protectedFunctions: [...protectedFns.keys()].filter((k) => protectedFns.get(k)),
226
+ issues,
227
+ };
228
+ }
229
+ /** 解析一个 Go 文件的顶层函数 */
230
+ function goFuncsOf(text, file) {
231
+ const out = [];
232
+ const pkgM = text.match(/^package\s+(\w+)/m);
233
+ const pkg = pkgM ? pkgM[1] : "";
234
+ const headerRe = /^func\s+([A-Za-z_]\w*)\s*\(/gm;
235
+ let m;
236
+ while ((m = headerRe.exec(text)) !== null) {
237
+ const hStart = m.index;
238
+ // 平衡取参数
239
+ let depth = 1;
240
+ let end = m.index + m[0].length;
241
+ while (end < text.length && depth > 0) {
242
+ if (text[end] === "(")
243
+ depth++;
244
+ else if (text[end] === ")")
245
+ depth--;
246
+ end++;
247
+ }
248
+ const paramsText = text.slice(m.index + m[0].length, end - 1);
249
+ // 找下一个 func 头作为 body 终点
250
+ const nxt = out.length ? text.indexOf("func ", end) : text.indexOf("\nfunc ", end);
251
+ const bodyEnd = (() => {
252
+ const nextHeader = text.slice(end).search(/^func\s/m);
253
+ return nextHeader === -1 ? text.length : end + nextHeader;
254
+ })();
255
+ const body = text.slice(end, bodyEnd);
256
+ const routerParams = [];
257
+ // 参数切分(顶层逗号)
258
+ let d = 0;
259
+ let cur = "";
260
+ const parts = [];
261
+ for (const ch of paramsText) {
262
+ if (ch === "(")
263
+ d++;
264
+ else if (ch === ")")
265
+ d--;
266
+ if (ch === "," && d === 0) {
267
+ parts.push(cur.trim());
268
+ cur = "";
269
+ }
270
+ else
271
+ cur += ch;
272
+ }
273
+ if (cur.trim())
274
+ parts.push(cur.trim());
275
+ for (const p of parts) {
276
+ // name *pkg.fiber.Router | name fiber.App …
277
+ const pm = p.match(/^([A-Za-z_]\w*)\s+[\w./*]*(\bfiber\b[\w./]*(?:Router|App|Group))/);
278
+ if (pm && /Router|App|Group/.test(pm[2]))
279
+ routerParams.push(pm[1]);
280
+ }
281
+ out.push({ file, pkg, name: m[1], routerParams, body: text.slice(end, bodyEnd) });
282
+ headerRe.lastIndex = bodyEnd; // 跳过函数体
283
+ }
284
+ return out;
285
+ }
286
+ /**
287
+ * 项目级保护函数集(多层 Register 链,包限定键):
288
+ * 队列自全部函数「参数未认证」种子起,凡函数体在「组已 Use 认证」后以
289
+ * 认证组调用项目内 Register 函数 → 该函数入队(参数认证)——支持
290
+ * journalist 式 main→api.Register(Group+Use)→v1.Register→模块 多层链。
291
+ * 键 = pkg:name,避免跨包同名函数(feeds.Register/tokens.Register…)串扰。
292
+ */
293
+ function fiberProjectProtectedFns(files) {
294
+ const funcs = [];
295
+ for (const f of files)
296
+ funcs.push(...goFuncsOf(f.text, f.file));
297
+ const keyOf = (pkg, name) => `${pkg}:${name}`;
298
+ const byKey = new Map();
299
+ for (const fn of funcs) {
300
+ byKey.set(keyOf(fn.pkg, fn.name), [...(byKey.get(keyOf(fn.pkg, fn.name)) || []), fn]);
301
+ }
302
+ const protectedFns = new Map();
303
+ const done = new Set();
304
+ const queue = [];
305
+ for (const fn of funcs)
306
+ queue.push({ pkg: fn.pkg, name: fn.name, authed: false });
307
+ while (queue.length) {
308
+ const { pkg, name, authed } = queue.shift();
309
+ const dk = `${pkg}|${name}|${authed}`;
310
+ if (done.has(dk))
311
+ continue;
312
+ done.add(dk);
313
+ if (authed)
314
+ protectedFns.set(keyOf(pkg, name), true);
315
+ const candidates = byKey.get(keyOf(pkg, name)) || [];
316
+ for (const fn of candidates) {
317
+ const authedParams = new Set();
318
+ if (authed)
319
+ fn.routerParams.forEach((pp) => authedParams.add(pp));
320
+ const callees = fiberBodyProtectedCalls(fn.body, authedParams, funcs);
321
+ for (const c of callees)
322
+ queue.push({ pkg: c.pkg, name: c.name, authed: true });
323
+ }
324
+ }
325
+ return protectedFns;
326
+ }
327
+ /**
328
+ * 函数体内事件模拟 v2:返回在其认证相位被调用的项目函数(包限定)。
329
+ * 调用限定符 feeds.Register → pkg 匹配 feeds;同包调用按当前函数包。
330
+ */
331
+ function fiberBodyProtectedCalls(body, authedParams, allFuncs) {
332
+ const calls = [];
333
+ const b = body.replace(/\(\s*\*\s*(\w+)\s*\)/g, "$1");
334
+ const curPkg = allFuncs.find((f) => f.body === body)?.pkg || "";
335
+ const state = new Map();
336
+ for (const p of authedParams)
337
+ state.set(p, true);
338
+ const re = /(\w+)\s*(?::=|=)\s*(?:[\w.]*\.)?([A-Za-z_]\w*)\.Group\s*\(|(\w+)\.Use\s*\(\s*([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)|(?:([A-Za-z_]\w*)\.)?([A-Z][A-Za-z0-9_]*)\s*\(/g;
339
+ let m;
340
+ while ((m = re.exec(b)) !== null) {
341
+ if (m[1] !== undefined) {
342
+ state.set(m[1], !!state.get(m[2]));
343
+ }
344
+ else if (m[3] !== undefined) {
345
+ if (isAuthFnName(m[4]))
346
+ state.set(m[3], true);
347
+ }
348
+ else if (m[5] !== undefined || m[6] !== undefined) {
349
+ const qual = m[5];
350
+ const fname = m[6];
351
+ // 项目内函数匹配:限定符 → pkg==qual;无限定符 → 同包
352
+ const matches = allFuncs.filter((f) => f.name === fname && (qual ? f.pkg === qual : f.pkg === curPkg));
353
+ if (matches.length === 0)
354
+ continue;
355
+ const openIdx = b.indexOf("(", m.index + m[0].length - 1);
356
+ if (openIdx < 0)
357
+ continue;
358
+ let depth = 1;
359
+ let i = openIdx + 1;
360
+ while (i < b.length && depth > 0) {
361
+ if (b[i] === "(")
362
+ depth++;
363
+ else if (b[i] === ")")
364
+ depth--;
365
+ i++;
366
+ }
367
+ const args = b.slice(openIdx + 1, i - 1).split(",").map((x) => x.trim().replace(/^[&*]+/, ""));
368
+ // 实参认证判定:直接组变量 或 内联组派生 X.Group(...)(X 已认证)
369
+ const argAuthed = (a) => {
370
+ if (state.get(a))
371
+ return true;
372
+ const g = a.match(/^([A-Za-z_]\w*)\.Group\s*\(/);
373
+ return !!g && !!state.get(g[1]);
374
+ };
375
+ if (args.some(argAuthed)) {
376
+ for (const mm of matches)
377
+ calls.push({ pkg: mm.pkg, name: mm.name });
378
+ }
379
+ }
380
+ }
381
+ return calls;
382
+ }
@@ -1,10 +1,46 @@
1
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
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  /**
4
37
  * fiber-detector.test.ts — Fiber 框架适配器规则回归(纯函数,无文件 I/O)
5
38
  */
6
39
  const vitest_1 = require("vitest");
7
40
  const fiber_detector_1 = require("./fiber-detector");
41
+ const fs = __importStar(require("fs"));
42
+ const os = __importStar(require("os"));
43
+ const path = __importStar(require("path"));
8
44
  const app = (routes, extra = "") => `
9
45
  import "github.com/gofiber/fiber/v2"
10
46
 
@@ -55,3 +91,155 @@ func main() {
55
91
  (0, vitest_1.expect)(issues).toHaveLength(0);
56
92
  });
57
93
  });
94
+ // ── V8 修复轮回归:窗口边界(单点摘保护不再被后续路由掩盖)──
95
+ (0, vitest_1.describe)("fiber-detector V8 修复回归", () => {
96
+ (0, vitest_1.it)("窗口不跨路由串扰:下一路由的 Protected 不掩盖上一路由摘保护", () => {
97
+ const { issues, routes } = (0, fiber_detector_1.analyzeFiberApp)(app(`
98
+ api.Post("/logout", logoutHandler)
99
+ api.Post("/refresh-token", middleware.Protected(), refreshHandler)
100
+ `));
101
+ const logout = routes.find((x) => x.path === "/logout");
102
+ const refresh = routes.find((x) => x.path === "/refresh-token");
103
+ (0, vitest_1.expect)(logout.protected).toBe(false);
104
+ (0, vitest_1.expect)(refresh.protected).toBe(true);
105
+ (0, vitest_1.expect)(issues.map((i) => i.route)).toContain("POST /logout");
106
+ (0, vitest_1.expect)(issues.map((i) => i.route)).not.toContain("POST /refresh-token");
107
+ });
108
+ (0, vitest_1.it)("handler 名含 auth 词不误判(logoutHandler 不被当认证)", () => {
109
+ const { routes } = (0, fiber_detector_1.analyzeFiberApp)(app(`
110
+ api.Post("/logout", authHandler.Logout)
111
+ `));
112
+ (0, vitest_1.expect)(routes.find((x) => x.path === "/logout").protected).toBe(false);
113
+ });
114
+ });
115
+ // ── Fiber 组认证跨文件传播(gin 同款模型移植)──
116
+ const FBOOT = `package main
117
+ import "github.com/gofiber/fiber/v2"
118
+ func main() {
119
+ app := fiber.New()
120
+ api := app.Group("/api")
121
+ users.UsersRegister(api.Group("/users"))
122
+ api.Use(middleware.Protected())
123
+ users.UserRegister(api.Group("/user"))
124
+ articles.ArticlesRegister(api.Group("/articles"))
125
+ }
126
+ `;
127
+ const FROUTERS = `package users
128
+ import "github.com/gofiber/fiber/v2"
129
+ func UsersRegister(router fiber.Router) {
130
+ router.Post("/login", UsersLogin)
131
+ router.Post("", UsersRegistration)
132
+ }
133
+ func UserRegister(router fiber.Router) {
134
+ router.Put("", UserUpdate)
135
+ }
136
+ `;
137
+ (0, vitest_1.describe)("fiberProtectedRegisterFns 组认证相位", () => {
138
+ (0, vitest_1.it)("Use 之后的 Register 受保护,Use 之前的公开", () => {
139
+ const p = (0, fiber_detector_1.fiberProtectedRegisterFns)(FBOOT);
140
+ (0, vitest_1.expect)(p.get("UserRegister")).toBe(true);
141
+ (0, vitest_1.expect)(p.get("ArticlesRegister")).toBe(true);
142
+ (0, vitest_1.expect)(p.get("UsersRegister")).toBeUndefined();
143
+ });
144
+ });
145
+ (0, vitest_1.describe)("fiberEnclosingFunc 归属", () => {
146
+ (0, vitest_1.it)("按 func 头行号归属", () => {
147
+ // FROUTERS:1 package / 2 import / 3 func UsersRegister / 6 func UserRegister
148
+ (0, vitest_1.expect)((0, fiber_detector_1.fiberEnclosingFunc)(FROUTERS, 4)).toBe("UsersRegister");
149
+ (0, vitest_1.expect)((0, fiber_detector_1.fiberEnclosingFunc)(FROUTERS, 7)).toBe("UserRegister");
150
+ });
151
+ });
152
+ (0, vitest_1.describe)("analyzeFiberProject 跨文件传播", () => {
153
+ function makeProject(withUse) {
154
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "fiber-proj-"));
155
+ const boot = withUse ? FBOOT : FBOOT.replace(/\s*api\.Use\(middleware\.Protected\(\)\)\n/, "");
156
+ fs.writeFileSync(path.join(dir, "main.go"), boot);
157
+ fs.writeFileSync(path.join(dir, "routers.go"), FROUTERS);
158
+ return dir;
159
+ }
160
+ (0, vitest_1.it)("Use 保护下跨文件 mutation 不报", () => {
161
+ const dir = makeProject(true);
162
+ try {
163
+ const a = (0, fiber_detector_1.analyzeFiberProject)(dir);
164
+ (0, vitest_1.expect)(a.issues.filter((i) => i.rule === "FIBER_ROUTE_NO_AUTH")).toHaveLength(0);
165
+ }
166
+ finally {
167
+ fs.rmSync(dir, { recursive: true, force: true });
168
+ }
169
+ });
170
+ (0, vitest_1.it)("删 Use → mutation 重现(敏感性保留)", () => {
171
+ const dir = makeProject(false);
172
+ try {
173
+ const a = (0, fiber_detector_1.analyzeFiberProject)(dir);
174
+ const routes = a.issues.filter((i) => i.rule === "FIBER_ROUTE_NO_AUTH").map((i) => i.route);
175
+ (0, vitest_1.expect)(routes).toContain("PUT "); // UserRegister mutation 重现
176
+ // POST "" 是 register(/login 姊妹佐证豁免,公开)——不报正确
177
+ }
178
+ finally {
179
+ fs.rmSync(dir, { recursive: true, force: true });
180
+ }
181
+ });
182
+ });
183
+ // ── 多层 Register 链(journalist 式 main→api(Group+Use)→v1→模块)──
184
+ function makeNestedProject(withUse) {
185
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "fiber-nest-"));
186
+ const mk = (rel, content) => {
187
+ const fp = path.join(dir, rel);
188
+ fs.mkdirSync(path.dirname(fp), { recursive: true });
189
+ fs.writeFileSync(fp, content);
190
+ };
191
+ mk("main/main.go", `package main
192
+ import "github.com/gofiber/fiber/v2"
193
+ func main() {
194
+ app := fiber.New()
195
+ api.Register(app)
196
+ }
197
+ `);
198
+ mk("api/api.go", `package api
199
+ import "github.com/gofiber/fiber/v2"
200
+ func Register(fiberApp *fiber.App) {
201
+ api := fiberApp.Group("/api")
202
+ ${withUse ? "\tapi.Use(middleware.Protected())\n" : ""}\tv1.Register(&api)
203
+ }
204
+ `);
205
+ mk("v1/v1.go", `package v1
206
+ import "github.com/gofiber/fiber/v2"
207
+ func Register(router *fiber.Router) {
208
+ feeds.Register(router)
209
+ }
210
+ `);
211
+ mk("feeds/feeds.go", `package feeds
212
+ import "github.com/gofiber/fiber/v2"
213
+ func Register(router *fiber.Router) {
214
+ router.Post("/", CreateFeed)
215
+ router.Put("/:id", UpdateFeed)
216
+ }
217
+ `);
218
+ return dir;
219
+ }
220
+ (0, vitest_1.describe)("analyzeFiberProject 多层 Register 链(journalist 式)", () => {
221
+ (0, vitest_1.it)("api(Group+Use)→v1→feeds 链:跨层 mutation 不报", () => {
222
+ const dir = makeNestedProject(true);
223
+ try {
224
+ const a = (0, fiber_detector_1.analyzeFiberProject)(dir);
225
+ (0, vitest_1.expect)(a.issues.filter((i) => i.rule === "FIBER_ROUTE_NO_AUTH")).toHaveLength(0);
226
+ (0, vitest_1.expect)(a.protectedFunctions).toContain("feeds:Register");
227
+ (0, vitest_1.expect)(a.protectedFunctions).toContain("v1:Register");
228
+ }
229
+ finally {
230
+ fs.rmSync(dir, { recursive: true, force: true });
231
+ }
232
+ });
233
+ (0, vitest_1.it)("删中间层 api.Use → feeds mutation 重现(敏感性穿透多层)", () => {
234
+ const dir = makeNestedProject(false);
235
+ try {
236
+ const a = (0, fiber_detector_1.analyzeFiberProject)(dir);
237
+ const routes = a.issues.filter((i) => i.rule === "FIBER_ROUTE_NO_AUTH").map((i) => i.route);
238
+ (0, vitest_1.expect)(routes).toContain("POST /");
239
+ (0, vitest_1.expect)(routes).toContain("PUT /:id");
240
+ }
241
+ finally {
242
+ fs.rmSync(dir, { recursive: true, force: true });
243
+ }
244
+ });
245
+ });