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.
@@ -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
+ });
@@ -54,7 +54,12 @@ var __importStar = (this && this.__importStar) || (function () {
54
54
  Object.defineProperty(exports, "__esModule", { value: true });
55
55
  exports.analyzeGinApp = analyzeGinApp;
56
56
  exports.analyzeGinFile = analyzeGinFile;
57
+ exports.ginFuncStarts = ginFuncStarts;
58
+ exports.ginProtectedRegisterFns = ginProtectedRegisterFns;
59
+ exports.ginEnclosingFunc = ginEnclosingFunc;
60
+ exports.analyzeGinProject = analyzeGinProject;
57
61
  const fs = __importStar(require("fs"));
62
+ const route_window_1 = require("./route-window");
58
63
  const MUTATION_METHODS = new Set(["post", "put", "patch", "delete"]);
59
64
  const AUTH_ENTRY_WORDS = [
60
65
  "login", "signin", "sign_in", "regist", "signup", "sign_up",
@@ -84,24 +89,28 @@ function analyzeGinApp(code) {
84
89
  return { hasGin: false, routes, authGroupMiddleware, issues };
85
90
  }
86
91
  // 组级认证中间件:r.Use(authMW) / r.Group("/api", authMW) / g.Use(authMW)
87
- const useRe = /\.Use\s*\(\s*([A-Za-z_][\w]*)/g;
92
+ // 捕获支持点限定成员(users.AuthMiddleware)——修复旧版只捕限定符
93
+ // "users" 的缺陷(V7)。整名送词表判定(AuthMiddleware 含 auth ✓)
94
+ const useRe = /\.Use\s*\(\s*([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)/g;
88
95
  let m;
89
96
  while ((m = useRe.exec(code)) !== null) {
90
97
  if (isAuthFnName(m[1]))
91
98
  authGroupMiddleware.push(m[1]);
92
99
  }
93
- const groupRe = /\.Group\s*\(\s*"[^"]*"\s*,\s*([A-Za-z_][\w]*)/g;
100
+ const groupRe = /\.Group\s*\(\s*"[^"]*"\s*,\s*([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)/g;
94
101
  while ((m = groupRe.exec(code)) !== null) {
95
102
  if (isAuthFnName(m[1]))
96
103
  authGroupMiddleware.push(m[1]);
97
104
  }
98
105
  // 路由注册:r.POST("/x", mw1, mw2, handler)——Go 方法名大写(POST 惯例)
99
- const routeRe = /\.(get|post|put|patch|delete)\s*\(\s*"([^"]+)"/gi;
106
+ // 空路径 "..." 允许(realworld 惯用 POST("", ...) 双注册)
107
+ const routeRe = /\.(get|post|put|patch|delete)\s*\(\s*"([^"]*)"(\s*,\s*|\s*\))/gi;
100
108
  while ((m = routeRe.exec(code)) !== null) {
101
109
  const method = m[1].toLowerCase();
102
110
  const pathName = m[2];
103
- const window = code.slice(m.index + m[0].length, m.index + m[0].length + 300);
104
- const mwNames = window.match(/[A-Za-z_][\w]*/g) || [];
111
+ // 认证窗口 = 本次调用边界内(括号感知),不跨路由(V7 缺陷修复)
112
+ const window = (0, route_window_1.routeCallWindow)(code, m.index + m[0].length);
113
+ const mwNames = (0, route_window_1.middlewareNamesFromWindow)(window);
105
114
  const hasAuthMw = mwNames.some((name) => isAuthFnName(name));
106
115
  routes.push({
107
116
  method,
@@ -109,15 +118,23 @@ function analyzeGinApp(code) {
109
118
  protected: hasAuthMw,
110
119
  line: code.slice(0, m.index).split("\n").length,
111
120
  });
112
- if (MUTATION_METHODS.has(method) && !hasAuthMw
113
- && authGroupMiddleware.length === 0 && !isAuthEntryPath(pathName)) {
121
+ }
122
+ // register 集合豁免(语义层,同 Koa):有 <path>/login 姊妹佐证的
123
+ // 账户集合,其无认证 POST = 公开注册(gin realworld 用 POST "" /
124
+ // "/" + /login 双注册——路径豁免词表认不出)
125
+ const registerRoots = (0, route_window_1.collectRegisterRoots)(routes.map((r) => r.path));
126
+ for (const r of routes) {
127
+ if (MUTATION_METHODS.has(r.method) && !r.protected
128
+ && authGroupMiddleware.length === 0
129
+ && !isAuthEntryPath(r.path)
130
+ && !(r.method === "post" && (0, route_window_1.isRegisterRoot)(r.path, registerRoots))) {
114
131
  issues.push({
115
132
  severity: "medium",
116
133
  rule: "GIN_ROUTE_NO_AUTH",
117
- message: `Route ${method.toUpperCase()} ${pathName} has no auth middleware ` +
134
+ message: `Route ${r.method.toUpperCase()} ${r.path} has no auth middleware ` +
118
135
  `and no auth Use/Group middleware — any caller can reach it.`,
119
- route: `${method.toUpperCase()} ${pathName}`,
120
- line: code.slice(0, m.index).split("\n").length,
136
+ route: `${r.method.toUpperCase()} ${r.path}`,
137
+ line: r.line,
121
138
  });
122
139
  }
123
140
  }
@@ -131,3 +148,99 @@ function analyzeGinFile(filePath) {
131
148
  return null;
132
149
  return analyzeGinApp(code);
133
150
  }
151
+ /** 顶层函数头(行号 1-based)——排除接收者方法(method receiver) */
152
+ function ginFuncStarts(text) {
153
+ const out = [];
154
+ const re = /^func\s+([A-Za-z_]\w*)\s*\(/gm;
155
+ let m;
156
+ while ((m = re.exec(text)) !== null) {
157
+ out.push({ name: m[1], line: text.slice(0, m.index).split("\n").length });
158
+ }
159
+ return out;
160
+ }
161
+ /**
162
+ * 从 bootstrap 文本(含 gin.Default/New 的文件)推导:
163
+ * 哪些 Register 函数在「组已施加认证 Use 之后」被调用(其路由受保护)。
164
+ * 语句顺序敏感:Use 之前注册的(register/login)不算。
165
+ */
166
+ function ginProtectedRegisterFns(bootstrapText) {
167
+ const state = new Map(); // 组变量 → 已认证
168
+ const protectedFns = new Map();
169
+ const re = /(\w+)\s*(?::=|=)\s*gin\.(Default|New)\s*\(|(\w+)\.Use\s*\(\s*([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)(?:\s*\(\s*(true|false))?|(\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;
170
+ let m;
171
+ // 组编号(按开括号序):1,2 根组 | 3,4,5 Use | 6,7 子组 | 8,9 注册调用
172
+ while ((m = re.exec(bootstrapText)) !== null) {
173
+ if (m[1] !== undefined) {
174
+ state.set(m[1], false); // 根组:gin.Default/New
175
+ }
176
+ else if (m[3] !== undefined) {
177
+ // 组.Use(认证);首参字面 false = 可选认证(public 读通行)不视为
178
+ // 保护——否则删掉 required Use 后 mutations 仍被可选 Use 掩盖(FN)
179
+ if (isAuthFnName(m[4]) && m[5] !== "false")
180
+ state.set(m[3], true);
181
+ }
182
+ else if (m[6] !== undefined) {
183
+ state.set(m[6], !!state.get(m[7])); // 子组继承父组状态
184
+ }
185
+ else if (m[8] !== undefined) {
186
+ if (state.get(m[9]))
187
+ protectedFns.set(m[8], true); // 认证相位调用的 Register fn
188
+ }
189
+ }
190
+ return protectedFns;
191
+ }
192
+ /** routeLine 所在顶层函数名(按 func 头归属) */
193
+ function ginEnclosingFunc(text, routeLine) {
194
+ let name = null;
195
+ for (const f of ginFuncStarts(text)) {
196
+ if (f.line <= routeLine)
197
+ name = f.name;
198
+ else
199
+ break;
200
+ }
201
+ return name;
202
+ }
203
+ /** 项目级分析:跨文件组认证传播 + 文件级判定兜底 */
204
+ function analyzeGinProject(projectRoot) {
205
+ const files = [];
206
+ // 简易 walk(.go,跳过 _test/vendor/node_modules)
207
+ const walk = (d) => {
208
+ for (const e of fs.readdirSync(d, { withFileTypes: true })) {
209
+ if (e.isDirectory()) {
210
+ if (["vendor", "node_modules", ".git"].includes(e.name))
211
+ continue;
212
+ walk(`${d}/${e.name}`);
213
+ }
214
+ else if (e.name.endsWith(".go") && !e.name.endsWith("_test.go")) {
215
+ const fp = `${d}/${e.name}`;
216
+ try {
217
+ files.push({ file: fp, text: fs.readFileSync(fp, "utf-8"), a: analyzeGinFile(fp) });
218
+ }
219
+ catch { /* skip unreadable */ }
220
+ }
221
+ }
222
+ };
223
+ if (fs.existsSync(projectRoot))
224
+ walk(projectRoot.replace(/\/$/, ""));
225
+ // bootstrap:含 gin.Default/gin.New 的文件(组认证相位在此推导)
226
+ const bootstrap = files.find((f) => /gin\.(Default|New)\s*\(/.test(f.text));
227
+ const protectedFns = bootstrap
228
+ ? ginProtectedRegisterFns(bootstrap.text)
229
+ : new Map();
230
+ const issues = [];
231
+ for (const { file, text, a } of files) {
232
+ if (!a || a.issues.length === 0)
233
+ continue;
234
+ for (const issue of a.issues) {
235
+ const fn = issue.line ? ginEnclosingFunc(text, issue.line) : null;
236
+ if (fn && protectedFns.get(fn))
237
+ continue; // 认证相位注册 → 受保护
238
+ issues.push({ ...issue, route: issue.route });
239
+ }
240
+ }
241
+ return {
242
+ filesScanned: files.length,
243
+ protectedFunctions: [...protectedFns.keys()].filter((k) => protectedFns.get(k)),
244
+ issues,
245
+ };
246
+ }
@@ -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
  * gin-detector.test.ts — Gin 框架适配器规则回归(纯函数,无文件 I/O)
5
38
  */
6
39
  const vitest_1 = require("vitest");
7
40
  const gin_detector_1 = require("./gin-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/gin-gonic/gin"
10
46
 
@@ -62,3 +98,150 @@ func main() {
62
98
  (0, vitest_1.expect)(issues).toHaveLength(0);
63
99
  });
64
100
  });
101
+ // ── V7 修复轮回归:Use 点限定捕获 / 窗口边界 / 空路径 ──
102
+ (0, vitest_1.describe)("gin-detector V7 修复回归", () => {
103
+ (0, vitest_1.it)("Use 点限定成员(users.AuthMiddleware)被识别为认证中间件(旧版只捕 'users')", () => {
104
+ const { issues, authGroupMiddleware } = (0, gin_detector_1.analyzeGinApp)(app(`
105
+ v1 := r.Group("/api")
106
+ v1.Use(users.AuthMiddleware(true))
107
+ v1.POST("/pay", createPayment)
108
+ `));
109
+ (0, vitest_1.expect)(authGroupMiddleware).toContain("users.AuthMiddleware");
110
+ (0, vitest_1.expect)(issues).toHaveLength(0);
111
+ });
112
+ (0, vitest_1.it)("窗口不跨路由串扰:后面路由的认证中间件不保护前面的公开路由", () => {
113
+ const { issues, routes } = (0, gin_detector_1.analyzeGinApp)(app(`
114
+ v1.POST("/users", UsersRegistration)
115
+ v1.POST("/articles", AuthMiddleware(), ArticleCreate)
116
+ `));
117
+ const reg = routes.find((x) => x.path === "/users");
118
+ const art = routes.find((x) => x.path === "/articles");
119
+ (0, vitest_1.expect)(reg.protected).toBe(false);
120
+ (0, vitest_1.expect)(art.protected).toBe(true);
121
+ (0, vitest_1.expect)(issues.map((i) => i.route)).toContain("POST /users");
122
+ (0, vitest_1.expect)(issues.map((i) => i.route)).not.toContain("POST /articles");
123
+ });
124
+ (0, vitest_1.it)("handler 名含 auth 词不误判为认证(UsersLogin 是 handler 不是中间件)", () => {
125
+ const { routes } = (0, gin_detector_1.analyzeGinApp)(app(`
126
+ v1.POST("/login", UsersLogin)
127
+ `));
128
+ const login = routes.find((x) => x.path === "/login");
129
+ (0, vitest_1.expect)(login.protected).toBe(false);
130
+ });
131
+ (0, vitest_1.it)("空路径注册可见(realworld 惯用 POST(\"\", ...))", () => {
132
+ const { routes } = (0, gin_detector_1.analyzeGinApp)(app(`
133
+ v1.POST("", UsersRegistration)
134
+ `));
135
+ (0, vitest_1.expect)(routes.some((x) => x.path === "")).toBe(true);
136
+ });
137
+ });
138
+ // ── register 集合豁免(语义层)──
139
+ (0, vitest_1.describe)("gin-detector register 集合豁免", () => {
140
+ (0, vitest_1.it)("有 /login 姊妹佐证:POST \"\"/\"/\"(公开注册双注册)不报", () => {
141
+ const { issues } = (0, gin_detector_1.analyzeGinApp)(app(`
142
+ v1.POST("", UsersRegistration)
143
+ v1.POST("/", UsersRegistration)
144
+ v1.POST("/login", UsersLogin)
145
+ `));
146
+ (0, vitest_1.expect)(issues.map((i) => i.route)).not.toContain("POST ");
147
+ (0, vitest_1.expect)(issues.map((i) => i.route)).not.toContain("POST /");
148
+ (0, vitest_1.expect)(issues.map((i) => i.route)).not.toContain("POST /login");
149
+ });
150
+ (0, vitest_1.it)("POST-only:同根的 PUT 不豁免(user-update 类仍查)", () => {
151
+ const { issues } = (0, gin_detector_1.analyzeGinApp)(app(`
152
+ v1.POST("", UsersRegistration)
153
+ v1.POST("/login", UsersLogin)
154
+ v1.PUT("", UserUpdate)
155
+ `));
156
+ (0, vitest_1.expect)(issues.map((i) => i.route)).not.toContain("POST ");
157
+ (0, vitest_1.expect)(issues.map((i) => i.route)).toContain("PUT ");
158
+ });
159
+ (0, vitest_1.it)("无姊妹佐证:POST \"/users\" 仍报(管理员建用户不豁免)", () => {
160
+ const { issues } = (0, gin_detector_1.analyzeGinApp)(app(`
161
+ v1.POST("/users", AdminCreateUser)
162
+ `));
163
+ (0, vitest_1.expect)(issues.map((i) => i.route)).toContain("POST /users");
164
+ });
165
+ });
166
+ // ── V7 转正:组认证跨文件传播 ──
167
+ const BOOTSTRAP = `package main
168
+ import "github.com/gin-gonic/gin"
169
+ func main() {
170
+ r := gin.Default()
171
+ v1 := r.Group("/api")
172
+ users.UsersRegister(v1.Group("/users")) // Use 之前 → 公开
173
+ v1.Use(users.AuthMiddleware(false)) // 可选认证
174
+ tags.TagsRegister(v1.Group("/tags"))
175
+ v1.Use(users.AuthMiddleware(true))
176
+ users.UserRegister(v1.Group("/user"))
177
+ articles.ArticlesRegister(v1.Group("/articles"))
178
+ }
179
+ `;
180
+ const ROUTERS = `package users
181
+ import "github.com/gin-gonic/gin"
182
+ func UsersRegister(router *gin.RouterGroup) {
183
+ router.POST("/login", UsersLogin)
184
+ router.POST("", UsersRegistration)
185
+ }
186
+ func UserRegister(router *gin.RouterGroup) {
187
+ router.PUT("", UserUpdate)
188
+ router.PUT("/", UserUpdate)
189
+ }
190
+ `;
191
+ (0, vitest_1.describe)("ginProtectedRegisterFns 组认证相位推导", () => {
192
+ (0, vitest_1.it)("Use(true) 之后的 Register 调用受保护,Use 之前的公开", () => {
193
+ const p = (0, gin_detector_1.ginProtectedRegisterFns)(BOOTSTRAP);
194
+ (0, vitest_1.expect)(p.get("UserRegister")).toBe(true);
195
+ (0, vitest_1.expect)(p.get("ArticlesRegister")).toBe(true);
196
+ (0, vitest_1.expect)(p.get("UsersRegister")).toBeUndefined(); // Use 之前注册(register/login 公开)
197
+ (0, vitest_1.expect)(p.get("TagsRegister")).toBeUndefined(); // 仅可选 Use(false) 之下
198
+ });
199
+ (0, vitest_1.it)("可选认证 Use(false) 不视为保护(否则删 required 后被掩盖)", () => {
200
+ const p = (0, gin_detector_1.ginProtectedRegisterFns)(BOOTSTRAP.replace("v1.Use(users.AuthMiddleware(true))\n", ""));
201
+ (0, vitest_1.expect)(p.get("UserRegister")).toBeUndefined();
202
+ (0, vitest_1.expect)(p.get("ArticlesRegister")).toBeUndefined();
203
+ });
204
+ });
205
+ (0, vitest_1.describe)("ginEnclosingFunc 路由函数归属", () => {
206
+ (0, vitest_1.it)("按 func 头行号归属", () => {
207
+ const lines = ROUTERS.split("\n");
208
+ const lineNo = (i) => i + 1;
209
+ const fnAt = (text, ln) => (0, gin_detector_1.ginEnclosingFunc)(text, ln);
210
+ // ROUTERS:1 package / 2 import / 3 func UsersRegister … 7 func UserRegister
211
+ (0, vitest_1.expect)(fnAt(ROUTERS, lineNo(3))).toBe("UsersRegister");
212
+ (0, vitest_1.expect)(fnAt(ROUTERS, lineNo(7))).toBe("UserRegister");
213
+ (0, vitest_1.expect)(fnAt(ROUTERS, lineNo(1))).toBeNull(); // package 行无函数
214
+ });
215
+ });
216
+ (0, vitest_1.describe)("analyzeGinProject 跨文件传播", () => {
217
+ function makeProject(withRequiredUse) {
218
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "gin-proj-"));
219
+ const boot = withRequiredUse ? BOOTSTRAP : BOOTSTRAP.replace(/\tv1\.Use\(users\.AuthMiddleware\(true\)\)\n/, "");
220
+ fs.writeFileSync(path.join(dir, "hello.go"), boot);
221
+ fs.writeFileSync(path.join(dir, "routers.go"), ROUTERS);
222
+ return dir;
223
+ }
224
+ (0, vitest_1.it)("Use(true) 保护下:跨文件 mutation 不报", () => {
225
+ const dir = makeProject(true);
226
+ try {
227
+ const a = (0, gin_detector_1.analyzeGinProject)(dir);
228
+ // UserRegister 的 PUT 受组认证保护(跨文件传播)
229
+ (0, vitest_1.expect)(a.issues.filter((i) => i.rule === "GIN_ROUTE_NO_AUTH")).toHaveLength(0);
230
+ }
231
+ finally {
232
+ fs.rmSync(dir, { recursive: true, force: true });
233
+ }
234
+ });
235
+ (0, vitest_1.it)("删掉 Use(true):mutation 重新被报(敏感性保留)", () => {
236
+ const dir = makeProject(false);
237
+ try {
238
+ const a = (0, gin_detector_1.analyzeGinProject)(dir);
239
+ const routes = a.issues.filter((i) => i.rule === "GIN_ROUTE_NO_AUTH").map((i) => i.route);
240
+ (0, vitest_1.expect)(routes).toContain("PUT ");
241
+ (0, vitest_1.expect)(routes).toContain("PUT /");
242
+ }
243
+ finally {
244
+ fs.rmSync(dir, { recursive: true, force: true });
245
+ }
246
+ });
247
+ });