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.
@@ -57,6 +57,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
57
57
  exports.analyzeFastifyApp = analyzeFastifyApp;
58
58
  exports.analyzeFastifyFile = analyzeFastifyFile;
59
59
  const fs = __importStar(require("fs"));
60
+ const route_window_1 = require("./route-window");
60
61
  const MUTATION_METHODS = new Set(["post", "put", "patch", "delete"]);
61
62
  const AUTH_ENTRY_WORDS = [
62
63
  "login", "signin", "sign_in", "regist", "signup", "sign_up",
@@ -74,7 +75,50 @@ function isAuthFnName(name) {
74
75
  const lower = name.toLowerCase();
75
76
  return AUTH_FN_WORDS.some((w) => lower.includes(w));
76
77
  }
77
- // ── Analysis(代码串级,镜像 express-detector) ──
78
+ // ── Analysis(代码串级) ──
79
+ /** 自 openIdx('{' 或 '(')取平衡块内文本(字符串感知) */
80
+ function balancedBlock(code, openIdx) {
81
+ const open = code[openIdx];
82
+ const close = open === "{" ? "}" : ")";
83
+ let depth = 1;
84
+ let end = openIdx + 1;
85
+ let quote = null;
86
+ while (end < code.length && depth > 0) {
87
+ const ch = code[end];
88
+ if (quote) {
89
+ if (ch === quote && code[end - 1] !== "\\")
90
+ quote = null;
91
+ }
92
+ else if (ch === '"' || ch === "'" || ch === "`") {
93
+ quote = ch;
94
+ }
95
+ else if (ch === open) {
96
+ depth++;
97
+ }
98
+ else if (ch === close) {
99
+ depth--;
100
+ }
101
+ end++;
102
+ }
103
+ return code.slice(openIdx + 1, Math.max(openIdx + 1, end - 1));
104
+ }
105
+ /** 认证选项名:位置形态(options 对象)与 object-form 路由均可出现 */
106
+ const AUTH_OPTION_NAMES = ["onRequest", "preHandler", "preValidation"];
107
+ /** 选项数组里是否有 auth-like 名(支持点限定 server.authenticate) */
108
+ function optionListHasAuth(listText) {
109
+ return listText.split(",").some((name) => isAuthFnName(name.trim()));
110
+ }
111
+ function hasAuthOptionInBlock(block) {
112
+ for (const opt of AUTH_OPTION_NAMES) {
113
+ const re = new RegExp(`\\b${opt}\\s*:\\s*\\[([^\\]]*)\\]`, "g");
114
+ let mm;
115
+ while ((mm = re.exec(block)) !== null) {
116
+ if (optionListHasAuth(mm[1]))
117
+ return true;
118
+ }
119
+ }
120
+ return false;
121
+ }
78
122
  function analyzeFastifyApp(code) {
79
123
  const issues = [];
80
124
  const routes = [];
@@ -84,44 +128,60 @@ function analyzeFastifyApp(code) {
84
128
  return { hasFastify: false, routes, authHooks, issues };
85
129
  }
86
130
  // 全局认证钩子:addHook('preHandler'|'preValidation'|'onRequest', authFn)
87
- const hookRe = /\.addHook\s*\(\s*['"](preHandler|preValidation|onRequest)['"]\s*,\s*([A-Za-z_$][\w$]*)/g;
131
+ const hookRe = /\.addHook\s*\(\s*['"](preHandler|preValidation|onRequest)['"]\s*,\s*([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)/g;
88
132
  let m;
89
133
  while ((m = hookRe.exec(code)) !== null) {
90
134
  if (isAuthFnName(m[2]))
91
135
  authHooks.push(m[2]);
92
136
  }
93
- // 路由注册:fastify.post('/x', {...}, handler) —— 捕获到 options 窗口
94
- const routeRe = /\.(get|post|put|patch|delete|route)\s*\(\s*['"]([^'"]+)['"]/g;
95
- while ((m = routeRe.exec(code)) !== null) {
96
- const method = m[1].toLowerCase();
97
- const pathName = m[2];
98
- // 从路由调用起点向后截取 400 字符窗口,查 preHandler/preValidation 选项
99
- const windowStart = m.index;
100
- const window = code.slice(windowStart, windowStart + 400);
101
- const hasAuthOption = /\b(preHandler|preValidation)\s*:/g.test(window)
102
- && (() => {
103
- // 选项值里出现 auth-like 函数名才认(保守)
104
- const optMatch = window.match(/\b(preHandler|preValidation)\s*:\s*\[([^\]]*)\]/);
105
- if (!optMatch)
106
- return false;
107
- return optMatch[2].split(",").some((name) => isAuthFnName(name.trim()));
108
- })();
137
+ const pushRoute = (method, pathName, protected_, at) => {
109
138
  routes.push({
110
139
  method,
111
140
  path: pathName,
112
- protected: hasAuthOption,
113
- line: code.slice(0, m.index).split("\n").length,
141
+ protected: protected_,
142
+ line: code.slice(0, at).split("\n").length,
114
143
  });
115
- if (MUTATION_METHODS.has(method) && !hasAuthOption
116
- && authHooks.length === 0 && !isAuthEntryPath(pathName)) {
144
+ };
145
+ // ── object-form 路由:server.route({ method, path, onRequest:[auth], ... })
146
+ // (fastify-realworld 20/20 用此形态——V2 recall 失明根因)
147
+ const objRe = /\.route\s*\(\s*\{/g;
148
+ while ((m = objRe.exec(code)) !== null) {
149
+ const block = balancedBlock(code, m.index + m[0].length - 1); // 自 '{'
150
+ const methodM = block.match(/method\s*:\s*['"]([^'"]+)['"]/);
151
+ if (!methodM)
152
+ continue;
153
+ const method = methodM[1].toLowerCase();
154
+ // path 可能是拼接 options.prefix + 'users/login'——取首个引号字面量
155
+ const pathM = block.match(/path\s*:\s*[^,'"\n]*['"]([^'"]+)['"]/);
156
+ const pathName = pathM ? pathM[1] : "";
157
+ const protected_ = hasAuthOptionInBlock(block);
158
+ pushRoute(method, pathName, protected_, m.index);
159
+ }
160
+ // ── 位置形态:fastify.post('/x', { preHandler: [auth] }, handler) ──
161
+ const posRe = /\.(get|post|put|patch|delete)\s*\(\s*['"]([^'"]+)['"]/g;
162
+ while ((m = posRe.exec(code)) !== null) {
163
+ const method = m[1].toLowerCase();
164
+ const pathName = m[2];
165
+ const window = code.slice(m.index, m.index + 400);
166
+ const protected_ = hasAuthOptionInBlock(window);
167
+ pushRoute(method, pathName, protected_, m.index);
168
+ }
169
+ // register 集合豁免(语义层,同 Koa/Gin):有 <path>/login 姊妹佐证的
170
+ // 账户集合,其无认证 POST = 公开注册
171
+ const registerRoots = (0, route_window_1.collectRegisterRoots)(routes.map((r) => r.path));
172
+ for (const r of routes) {
173
+ if (MUTATION_METHODS.has(r.method) && !r.protected
174
+ && authHooks.length === 0
175
+ && !isAuthEntryPath(r.path)
176
+ && !(r.method === "post" && (0, route_window_1.isRegisterRoot)(r.path, registerRoots))) {
117
177
  issues.push({
118
178
  severity: "medium",
119
179
  rule: "FASTIFY_ROUTE_NO_AUTH",
120
- message: `Route ${method.toUpperCase()} ${pathName} is registered without ` +
121
- `preHandler/preValidation auth and the app has no auth hook — ` +
180
+ message: `Route ${r.method.toUpperCase()} ${r.path} is registered without ` +
181
+ `onRequest/preHandler/preValidation auth and the app has no auth hook — ` +
122
182
  `any caller can reach it.`,
123
- route: `${method.toUpperCase()} ${pathName}`,
124
- line: code.slice(0, m.index).split("\n").length,
183
+ route: `${r.method.toUpperCase()} ${r.path}`,
184
+ line: r.line,
125
185
  });
126
186
  }
127
187
  }
@@ -131,7 +191,11 @@ function analyzeFastifyFile(filePath) {
131
191
  if (!fs.existsSync(filePath))
132
192
  return null;
133
193
  const code = fs.readFileSync(filePath, "utf-8");
134
- if (!/from\s+['"]fastify['"]|require\(['"]fastify['"]\)/.test(code))
194
+ // 门:直接 import fastify(应用入口)或 fastify-plugin(真实插件模块——
195
+ // fastify-realworld 的路由模块是 fp(plugin) 包裹、接收 server 实例;
196
+ // 旧门只认 require('fastify') → 0/38 文件进门)
197
+ if (!/from\s+['"]fastify['"]|require\(['"]fastify['"]\)|fastify-plugin/.test(code)) {
135
198
  return null;
199
+ }
136
200
  return analyzeFastifyApp(code);
137
201
  }
@@ -1,4 +1,37 @@
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
  * fastify-detector.test.ts — Fastify 框架适配器规则回归(纯函数,无文件 I/O)
@@ -8,6 +41,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
8
41
  */
9
42
  const vitest_1 = require("vitest");
10
43
  const fastify_detector_1 = require("./fastify-detector");
44
+ const fs = __importStar(require("fs"));
45
+ const os = __importStar(require("os"));
46
+ const path = __importStar(require("path"));
11
47
  const app = (routes, hooks = "") => `
12
48
  import Fastify from "fastify";
13
49
  const fastify = Fastify();
@@ -60,3 +96,66 @@ fastify.post("/register", async (req, reply) => ({ ok: true }));
60
96
  (0, vitest_1.expect)(issues).toHaveLength(0);
61
97
  });
62
98
  });
99
+ // ── V2 结构性重写回归:object-form / onRequest / plugin 门 / register 豁免 ──
100
+ (0, vitest_1.describe)("fastify-detector object-form 路由(V2 修复回归)", () => {
101
+ (0, vitest_1.it)("server.route({method,path,onRequest:[server.authenticate]}) 受保护不报", () => {
102
+ const { issues, routes } = (0, fastify_detector_1.analyzeFastifyApp)(app(`
103
+ server.route({
104
+ method: 'POST',
105
+ path: options.prefix + 'articles',
106
+ onRequest: [server.authenticate],
107
+ handler: onCreate
108
+ });
109
+ `));
110
+ (0, vitest_1.expect)(routes.find((r) => r.path === "articles").protected).toBe(true);
111
+ (0, vitest_1.expect)(issues).toHaveLength(0);
112
+ });
113
+ (0, vitest_1.it)("object-form 无认证 mutation → 报", () => {
114
+ const { issues } = (0, fastify_detector_1.analyzeFastifyApp)(app(`
115
+ server.route({
116
+ method: 'POST',
117
+ path: options.prefix + 'payments',
118
+ handler: onPay
119
+ });
120
+ `));
121
+ (0, vitest_1.expect)(issues.map((i) => i.route)).toContain("POST payments");
122
+ });
123
+ (0, vitest_1.it)("点限定 server.authenticate 在 onRequest 数组被识别(词表含 auth)", () => {
124
+ const { routes } = (0, fastify_detector_1.analyzeFastifyApp)(app(`
125
+ server.route({ method: 'DELETE', path: 'x', onRequest: [server.authenticate], handler: h });
126
+ `));
127
+ (0, vitest_1.expect)(routes[0].protected).toBe(true);
128
+ });
129
+ (0, vitest_1.it)("register 集合豁免:POST users(有 users/login 姊妹)不报", () => {
130
+ const { issues } = (0, fastify_detector_1.analyzeFastifyApp)(app(`
131
+ server.route({ method: 'POST', path: options.prefix + 'users/login', handler: onLogin });
132
+ server.route({ method: 'POST', path: options.prefix + 'users', handler: onRegister });
133
+ `));
134
+ (0, vitest_1.expect)(issues.map((i) => i.route)).not.toContain("POST users");
135
+ (0, vitest_1.expect)(issues.map((i) => i.route)).not.toContain("POST users/login");
136
+ });
137
+ });
138
+ (0, vitest_1.describe)("fastify-detector plugin 门(V2 修复回归)", () => {
139
+ (0, vitest_1.it)("fastify-plugin 模块(fp(plugin) 路由文件)现可被分析", () => {
140
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "fastify-det-"));
141
+ try {
142
+ const fp = path.join(dir, "routes-users.js");
143
+ fs.writeFileSync(fp, `
144
+ const fp = require('fastify-plugin')
145
+ async function users (server, options, done) {
146
+ server.route({ method: 'POST', path: 'articles', onRequest: [server.authenticate], handler: h })
147
+ server.route({ method: 'POST', path: 'open', handler: h })
148
+ }
149
+ module.exports = fp(users)
150
+ `);
151
+ const a = (0, fastify_detector_1.analyzeFastifyFile)(fp);
152
+ (0, vitest_1.expect)(a).not.toBeNull();
153
+ (0, vitest_1.expect)(a.routes.length).toBe(2);
154
+ (0, vitest_1.expect)(a.issues.map((i) => i.route)).toContain("POST open");
155
+ (0, vitest_1.expect)(a.issues.map((i) => i.route)).not.toContain("POST articles");
156
+ }
157
+ finally {
158
+ fs.rmSync(dir, { recursive: true, force: true });
159
+ }
160
+ });
161
+ });
@@ -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
+ }