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.
@@ -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
+ });
@@ -56,6 +56,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
56
56
  exports.analyzeHapiApp = analyzeHapiApp;
57
57
  exports.analyzeHapiFile = analyzeHapiFile;
58
58
  const fs = __importStar(require("fs"));
59
+ const route_window_1 = require("./route-window");
59
60
  const MUTATION_METHODS = new Set(["post", "put", "patch", "delete"]);
60
61
  const AUTH_ENTRY_WORDS = [
61
62
  "login", "signin", "sign_in", "regist", "signup", "sign_up",
@@ -66,11 +67,56 @@ function isAuthEntryPath(pathName) {
66
67
  return AUTH_ENTRY_WORDS.some((w) => lower.includes(w));
67
68
  }
68
69
  // ── Analysis(代码串级) ──
70
+ /** 声明式 hapi 路由模块(V6 遗留缺口):真实 hapi 应用(glue/pal/插件)
71
+ * 以数组声明路由——module.exports = (server) => [ { method, path,
72
+ * config: { auth: 'jwt', ... }, handler }, ... ],由框架注册;
73
+ * 文件本身无 require('hapi')、无 server.route 调用 */
74
+ function isDeclarativeHapiModule(code) {
75
+ const hasRouteObject = /method\s*:\s*['"](GET|POST|PUT|PATCH|DELETE|get|post|put|patch|delete)['"]/.test(code) &&
76
+ /path\s*:\s*['"]/.test(code) &&
77
+ /\b(?:config|options)\s*:/.test(code);
78
+ if (!hasRouteObject)
79
+ return false;
80
+ return (/module\.exports\s*=\s*\(?\s*server\b/.test(code) ||
81
+ /module\.exports\s*=\s*function\s*\(\s*server\b/.test(code) ||
82
+ /return\s*\[/.test(code));
83
+ }
84
+ /** 自 routeObjStart('{') 取平衡块文本(含嵌套 config 对象;字符串感知) */
85
+ function hapiBalancedBlock(code, openIdx) {
86
+ let depth = 1;
87
+ let end = openIdx + 1;
88
+ let quote = null;
89
+ while (end < code.length && depth > 0) {
90
+ const ch = code[end];
91
+ if (quote) {
92
+ if (ch === quote && code[end - 1] !== "\\")
93
+ quote = null;
94
+ }
95
+ else if (ch === '"' || ch === "'" || ch === "`") {
96
+ quote = ch;
97
+ }
98
+ else if (ch === "{") {
99
+ depth++;
100
+ }
101
+ else if (ch === "}") {
102
+ depth--;
103
+ }
104
+ end++;
105
+ }
106
+ return code.slice(openIdx + 1, Math.max(openIdx + 1, end - 1));
107
+ }
108
+ function hapiAuthOption(block) {
109
+ const authM = block.match(/\bauth\s*:\s*(?:['"]([^'"]+)['"]|\{\s*strategy\s*:\s*['"]([^'"]+)['"]|\s*(false|true))/);
110
+ if (!authM)
111
+ return null;
112
+ return authM[3] === "false" ? "false" : authM[1] || authM[2] || authM[3] || null;
113
+ }
69
114
  function analyzeHapiApp(code) {
70
115
  const issues = [];
71
116
  const routes = [];
72
117
  const strategies = [];
73
- const hasHapi = /@hapi\/hapi|\bHapi\.server\b|\bhapi\.server\b/.test(code);
118
+ // @hapi-scoped(v17+)、v16 require('hapi'),或声明式数组模块(V6 修复)
119
+ const hasHapi = /@hapi\/hapi|\bHapi\.server\b|\bhapi\.server\b|require\(\s*['"]hapi['"]\s*\)|from\s+['"]hapi['"]/.test(code) || isDeclarativeHapiModule(code);
74
120
  if (!hasHapi) {
75
121
  return { hasHapi: false, routes, strategies, issues };
76
122
  }
@@ -80,43 +126,69 @@ function analyzeHapiApp(code) {
80
126
  while ((m = strategyRe.exec(code)) !== null) {
81
127
  strategies.push(m[1]);
82
128
  }
83
- // 路由块:server.route({ ... }) —— 从 route( 向后截 500 字符窗口
84
- const routeRe = /\.route\s*\(\s*\{/g;
85
- while ((m = routeRe.exec(code)) !== null) {
86
- const window = code.slice(m.index, m.index + 500);
87
- const methodM = window.match(/method\s*:\s*['"]([^'"]+)['"]/);
88
- const pathM = window.match(/path\s*:\s*['"]([^'"]+)['"]/);
89
- const authM = window.match(/auth\s*:\s*(?:['"]([^'"]+)['"]|\{\s*strategy\s*:\s*['"]([^'"]+)['"]|\s*(false|true))/);
90
- if (!methodM || !pathM)
129
+ const declarative = isDeclarativeHapiModule(code);
130
+ const lineAt = (idx) => code.slice(0, idx).split("\n").length;
131
+ if (!declarative) {
132
+ // 直连形态:server.route({ ... }) —— 500 字符窗口
133
+ const routeRe = /\.route\s*\(\s*\{/g;
134
+ while ((m = routeRe.exec(code)) !== null) {
135
+ const window = code.slice(m.index, m.index + 500);
136
+ const methodM = window.match(/method\s*:\s*['"]([^'"]+)['"]/);
137
+ const pathM = window.match(/path\s*:\s*['"]([^'"]+)['"]/);
138
+ if (!methodM || !pathM)
139
+ continue;
140
+ routes.push({
141
+ method: methodM[1].toLowerCase(),
142
+ path: pathM[1],
143
+ authOption: hapiAuthOption(window),
144
+ line: lineAt(m.index),
145
+ });
146
+ }
147
+ }
148
+ else {
149
+ // 声明式数组:每个 { method, path, config:{auth} } 路由对象
150
+ const verbRe = /method\s*:\s*['"](GET|POST|PUT|PATCH|DELETE|get|post|put|patch|delete)['"]/g;
151
+ while ((m = verbRe.exec(code)) !== null) {
152
+ const verb = m[1].toLowerCase();
153
+ const verbLine = lineAt(m.index);
154
+ const objStart = code.lastIndexOf("{", m.index);
155
+ if (objStart < 0)
156
+ continue;
157
+ const block = hapiBalancedBlock(code, objStart);
158
+ const pathM = block.match(/path\s*:\s*['"]([^'"]+)['"]/);
159
+ if (!pathM)
160
+ continue;
161
+ // 重复对象去重
162
+ if (routes.some((r) => r.line === verbLine && r.method === verb))
163
+ continue;
164
+ routes.push({
165
+ method: verb,
166
+ path: pathM[1],
167
+ authOption: hapiAuthOption(block),
168
+ line: verbLine,
169
+ });
170
+ }
171
+ }
172
+ // register 集合豁免(语义层,同其他框架):users/login 姊妹 → POST users 公开
173
+ const registerRoots = (0, route_window_1.collectRegisterRoots)(routes.map((r) => r.path));
174
+ for (const r of routes) {
175
+ if (!MUTATION_METHODS.has(r.method))
91
176
  continue;
92
- const method = methodM[1].toLowerCase();
93
- const pathName = pathM[1];
94
- // auth: false → 显式公开;无 auth 字段 → authM null
95
- const authOption = authM
96
- ? (authM[3] === "false" ? "false" : authM[1] || authM[2] || authM[3] || null)
97
- : null;
98
- routes.push({
99
- method,
100
- path: pathName,
101
- authOption,
102
- line: code.slice(0, m.index).split("\n").length,
103
- });
104
- if (!MUTATION_METHODS.has(method))
177
+ if (isAuthEntryPath(r.path))
105
178
  continue;
106
- if (isAuthEntryPath(pathName))
179
+ if (r.method === "post" && (0, route_window_1.isRegisterRoot)(r.path, registerRoots))
107
180
  continue;
108
- // auth 字段(authOption null 且非显式 false 已涵盖)或显式 false → 报
109
- if (authOption === null || authOption === "false") {
181
+ if (r.authOption === null || r.authOption === "false") {
110
182
  issues.push({
111
183
  severity: "medium",
112
184
  rule: "HAPI_ROUTE_NO_AUTH",
113
- message: authOption === "false"
114
- ? `Mutation route ${method.toUpperCase()} ${pathName} is explicitly ` +
185
+ message: r.authOption === "false"
186
+ ? `Mutation route ${r.method.toUpperCase()} ${r.path} is explicitly ` +
115
187
  `public (auth: false) — any caller can reach it.`
116
- : `Mutation route ${method.toUpperCase()} ${pathName} has no auth ` +
188
+ : `Mutation route ${r.method.toUpperCase()} ${r.path} has no auth ` +
117
189
  `option in its route config — any caller can reach it.`,
118
- route: `${method.toUpperCase()} ${pathName}`,
119
- line: code.slice(0, m.index).split("\n").length,
190
+ route: `${r.method.toUpperCase()} ${r.path}`,
191
+ line: r.line,
120
192
  });
121
193
  }
122
194
  }
@@ -126,7 +198,9 @@ function analyzeHapiFile(filePath) {
126
198
  if (!fs.existsSync(filePath))
127
199
  return null;
128
200
  const code = fs.readFileSync(filePath, "utf-8");
129
- if (!/@hapi\/hapi|@hapi\/hawk|\bHapi\.server\b/.test(code))
201
+ // gate:@hapi-scoped / v16 require('hapi') / 声明式数组模块
202
+ const marker = /@hapi\/hapi|@hapi\/hawk|\bHapi\.server\b|require\(\s*['"]hapi['"]\s*\)|from\s+['"]hapi['"]/.test(code) || isDeclarativeHapiModule(code);
203
+ if (!marker)
130
204
  return null;
131
205
  return analyzeHapiApp(code);
132
206
  }
@@ -58,3 +58,94 @@ server.route({ method: "POST", path: "/login", handler: () => "token" });
58
58
  (0, vitest_1.expect)(issues).toHaveLength(0);
59
59
  });
60
60
  });
61
+ // ── V6 修复轮回归:v16 时代 require('hapi') gate 兼容 ──
62
+ (0, vitest_1.describe)("hapi-detector V6 gate 修复回归", () => {
63
+ (0, vitest_1.it)("v16 形态 require('hapi') + server.route 可被分析(旧 gate 只认 @hapi-scoped)", () => {
64
+ const code = `
65
+ const Hapi = require("hapi");
66
+ const server = new Hapi.Server();
67
+ server.connection({ port: 3000 });
68
+ server.auth.strategy("jwt", "jwt", { key: "s" });
69
+ server.route({ method: "POST", path: "/articles", config: { auth: "jwt" }, handler: (r, reply) => reply({}) });
70
+ server.route({ method: "POST", path: "/payments", handler: (r, reply) => reply({}) });
71
+ `;
72
+ const { hasHapi, strategies, routes, issues } = (0, hapi_detector_1.analyzeHapiApp)(code);
73
+ (0, vitest_1.expect)(hasHapi).toBe(true);
74
+ (0, vitest_1.expect)(strategies).toContain("jwt");
75
+ const articles = routes.find((r) => r.path === "/articles");
76
+ const payments = routes.find((r) => r.path === "/payments");
77
+ (0, vitest_1.expect)(articles).toBeDefined();
78
+ (0, vitest_1.expect)(articles.authOption).toBe("jwt"); // config.auth 嵌套亦被窗口文本捕获
79
+ (0, vitest_1.expect)(issues.map((i) => i.rule)).toContain("HAPI_ROUTE_NO_AUTH");
80
+ (0, vitest_1.expect)(issues.map((i) => i.route)).not.toContain("POST /articles");
81
+ });
82
+ (0, vitest_1.it)("gate 不误收 hapi-auth-jwt2(require('hapi') 需闭合引号紧随)", () => {
83
+ const { hasHapi } = (0, hapi_detector_1.analyzeHapiApp)(`
84
+ const hapiAuth = require("hapi-auth-jwt2");
85
+ module.exports = (server) => { return []; };
86
+ `);
87
+ (0, vitest_1.expect)(hasHapi).toBe(false);
88
+ });
89
+ });
90
+ // ── V6 遗留缺口:声明式数组路由 + config.auth 嵌套 ──
91
+ const DECLARATIVE = `
92
+ module.exports = (server) => {
93
+ const handlers = require('./handlers')(server)
94
+ return [
95
+ // GET 公开
96
+ {
97
+ method: 'GET',
98
+ path: '/articles',
99
+ config: { description: 'list' },
100
+ handler: handlers.list
101
+ },
102
+ // mutation 受保护(config.auth 嵌套)
103
+ {
104
+ method: 'POST',
105
+ path: '/articles',
106
+ config: { auth: 'jwt', response: {} },
107
+ handler: handlers.create
108
+ },
109
+ // 无认证 mutation
110
+ {
111
+ method: 'POST',
112
+ path: '/payments',
113
+ config: {},
114
+ handler: handlers.pay
115
+ }
116
+ ]
117
+ }
118
+ `;
119
+ const DECLARATIVE_USERS = `
120
+ module.exports = (server) => {
121
+ return [
122
+ { method: 'POST', path: '/users/login', config: {}, handler: h },
123
+ { method: 'POST', path: '/users', config: {}, handler: h },
124
+ { method: 'PUT', path: '/user', config: { auth: 'jwt' }, handler: h }
125
+ ]
126
+ }
127
+ `;
128
+ (0, vitest_1.describe)("hapi 声明式数组路由(V6 修复回归)", () => {
129
+ (0, vitest_1.it)("module.exports=(server)+数组路由对象被识别,config.auth 保护生效", () => {
130
+ const { hasHapi, routes, issues } = (0, hapi_detector_1.analyzeHapiApp)(DECLARATIVE);
131
+ (0, vitest_1.expect)(hasHapi).toBe(true);
132
+ (0, vitest_1.expect)(routes.length).toBe(3);
133
+ const create = routes.find((r) => r.method === "post" && r.path === "/articles");
134
+ (0, vitest_1.expect)(create.authOption).toBe("jwt");
135
+ const missing = issues.filter((i) => i.rule === "HAPI_ROUTE_NO_AUTH").map((i) => i.route);
136
+ (0, vitest_1.expect)(missing).toContain("POST /payments");
137
+ (0, vitest_1.expect)(missing).not.toContain("POST /articles");
138
+ });
139
+ (0, vitest_1.it)("摘 config.auth → mutation 报(敏感性)", () => {
140
+ const stripped = DECLARATIVE.replace("config: { auth: 'jwt', response: {} }", "config: { response: {} }");
141
+ const { issues } = (0, hapi_detector_1.analyzeHapiApp)(stripped);
142
+ (0, vitest_1.expect)(issues.some((i) => i.rule === "HAPI_ROUTE_NO_AUTH" && i.route === "POST /articles")).toBe(true);
143
+ });
144
+ (0, vitest_1.it)("register/login 公开:users/login + users(姊妹佐证)不报", () => {
145
+ const { issues } = (0, hapi_detector_1.analyzeHapiApp)(DECLARATIVE_USERS);
146
+ const missing = issues.filter((i) => i.rule === "HAPI_ROUTE_NO_AUTH").map((i) => i.route);
147
+ (0, vitest_1.expect)(missing).not.toContain("POST /users/login");
148
+ (0, vitest_1.expect)(missing).not.toContain("POST /users");
149
+ (0, vitest_1.expect)(missing).not.toContain("PUT /user");
150
+ });
151
+ });