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.
@@ -26,7 +26,9 @@ exports.extractRouterNames = extractRouterNames;
26
26
  exports.extractProcedures = extractProcedures;
27
27
  exports.analyzeTRPCFile = analyzeTRPCFile;
28
28
  // ── Detection Patterns ──
29
- const PROCEDURE_TYPE_PATTERN = /\b(publicProcedure|protectedProcedure|adminProcedure)\b/g;
29
+ // 注意:detect 用途不带 /g——带 /g 的 test() 会跨文件泄漏 lastIndex,
30
+ // 导致逐文件扫描结果随顺序漂移(实测 4/19 vs 7/19)
31
+ const PROCEDURE_TYPE_PATTERN = /\b(publicProcedure|protectedProcedure|adminProcedure)\b/;
30
32
  const DB_WRITE_PATTERN = /\b(db\.(insert|update|delete|create|upsert|execute)|prisma\.\w+\.(create|update|delete|upsert|createMany|updateMany|deleteMany)|drizzle\.(insert|update|delete)|\.(insert|update|delete|create|upsert)\s*\()/i;
31
33
  /**
32
34
  * Detect whether this file contains tRPC definitions.
@@ -57,30 +59,94 @@ function extractRouterNames(code) {
57
59
  */
58
60
  function extractProcedures(code) {
59
61
  const procedures = [];
60
- // Match: name: <procedureType>.input(...).<kind>( or name: <procedureType>.<kind>(
61
- const procRe = /([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(publicProcedure|protectedProcedure|adminProcedure)((?:\.\w+\s*\([^()]*\))*)\.(query|mutation)\s*\(/g;
62
+ // 过程起点:name: <procedureType>(不含链)——v11 惯用法 t.procedure
63
+ // (V4 遗留缺口:只认 XxxProcedure 命名包装,内联 t.procedure 不可见)
64
+ const procStartRe = /([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(t\s*\.\s*procedure|publicProcedure|protectedProcedure|adminProcedure)\b/g;
62
65
  let m;
63
- while ((m = procRe.exec(code)) !== null) {
66
+ while ((m = procStartRe.exec(code)) !== null) {
64
67
  const name = m[1];
65
- const procType = m[2].replace(/Procedure$/, "");
66
- const chain = m[3] || "";
67
- const kind = m[4];
68
- const hasInputSchema = /\.input\s*\(/.test(chain);
69
- // Extract body until matching closing paren of the query/mutation call.
70
- // Heuristic: scan forward for the closing paren matching the one after
71
- // the .query( / .mutation( token.
72
- const openIdx = procRe.lastIndex - 1; // index of '(' after query/mutation
68
+ // t.procedure = v11 基础构造器(无包装 → 默认公开语义);命名包装去掉后缀
69
+ const procType = (m[2].includes(".") ? "public" : m[2].replace(/Procedure$/, ""));
70
+ // ── 链扫描(括号感知)──
71
+ // procedure 类型后逐个解析 .method(balancedArgs),容忍嵌套括号与
72
+ // 多行(.input(z.object({...})) 等标准形态),直至 .query(/.mutation(
73
+ // 或链中断。旧实现用 (?:\.\w+\([^()]*\))* 不跨嵌套括号 标准
74
+ // zod input 链整体失明(V4 缺陷)。
75
+ let pos = procStartRe.lastIndex;
76
+ let hasInputSchema = false;
77
+ let kind = null;
78
+ let kindOpenIdx = -1; // .query( 或 .mutation( 的 '(' 下标
79
+ const skipWs = () => {
80
+ while (pos < code.length && /\s/.test(code[pos]))
81
+ pos++;
82
+ };
83
+ const consumeBalanced = (open, close) => {
84
+ let depth = 1;
85
+ let quote = null;
86
+ pos++; // 跳过 open
87
+ while (pos < code.length && depth > 0) {
88
+ const ch = code[pos];
89
+ if (quote) {
90
+ if (ch === quote && code[pos - 1] !== "\\")
91
+ quote = null;
92
+ }
93
+ else if (ch === '"' || ch === "'" || ch === "`") {
94
+ quote = ch;
95
+ }
96
+ else if (ch === open) {
97
+ depth++;
98
+ }
99
+ else if (ch === close) {
100
+ depth--;
101
+ }
102
+ pos++;
103
+ }
104
+ };
105
+ for (let step = 0; step < 100; step++) {
106
+ skipWs();
107
+ if (code[pos] !== ".")
108
+ break;
109
+ pos++;
110
+ const methStart = pos;
111
+ while (pos < code.length && /[A-Za-z0-9_$]/.test(code[pos]))
112
+ pos++;
113
+ const method = code.slice(methStart, pos);
114
+ skipWs();
115
+ if (code[pos] !== "(")
116
+ break;
117
+ if (method === "query" || method === "mutation") {
118
+ kind = method;
119
+ kindOpenIdx = pos; // '(' 位置
120
+ break;
121
+ }
122
+ if (method === "input")
123
+ hasInputSchema = true;
124
+ consumeBalanced("(", ")");
125
+ }
126
+ if (kind === null || kindOpenIdx < 0)
127
+ continue; // 非完整过程定义
128
+ // ── body:自 kind 的 '(' 后到匹配闭合括号(字符串感知)──
73
129
  let depth = 1;
74
- let closeIdx = openIdx + 1;
130
+ let closeIdx = kindOpenIdx + 1;
131
+ let quote = null;
75
132
  while (closeIdx < code.length && depth > 0) {
76
133
  const ch = code[closeIdx];
77
- if (ch === "(")
134
+ if (quote) {
135
+ if (ch === quote && code[closeIdx - 1] !== "\\")
136
+ quote = null;
137
+ }
138
+ else if (ch === '"' || ch === "'" || ch === "`") {
139
+ quote = ch;
140
+ }
141
+ else if (ch === "(") {
78
142
  depth++;
79
- else if (ch === ")")
143
+ }
144
+ else if (ch === ")") {
80
145
  depth--;
146
+ }
81
147
  closeIdx++;
82
148
  }
83
- const body = code.slice(openIdx + 1, Math.min(closeIdx - 1, openIdx + 2000));
149
+ const body = code.slice(kindOpenIdx + 1, Math.min(closeIdx - 1, kindOpenIdx + 2000));
84
150
  const usesInputInBody = /input\s*\./.test(body) || /\binput\b/.test(body);
85
151
  const doesDbWrite = DB_WRITE_PATTERN.test(body);
86
152
  procedures.push({
@@ -0,0 +1,177 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ /**
37
+ * trpc-detector.test.ts — tRPC 检测器回归(纯函数,无文件 I/O)
38
+ *
39
+ * 覆盖 V4 真实语料(netflx-web)暴露的两项缺陷:
40
+ * 1. 链匹配正则不跨嵌套括号 → 标准 .input(z.object({...})) 过程失明
41
+ * 2. PROCEDURE_TYPE_PATTERN /g lastIndex 泄漏 → 逐文件扫描漂移
42
+ */
43
+ const vitest_1 = require("vitest");
44
+ const trpc_detector_1 = require("./trpc-detector");
45
+ const fs = __importStar(require("fs"));
46
+ const os = __importStar(require("os"));
47
+ const path = __importStar(require("path"));
48
+ // ── 修复 1:标准 .input 链(嵌套括号/多行)必须可见 ──
49
+ const ROUTER_WITH_INPUT = `
50
+ const t = initTRPC.context<{ db: Db }>().create();
51
+ export const postRouter = t.router({
52
+ addComment: protectedProcedure
53
+ .input(
54
+ z.object({
55
+ articleId: z.string(),
56
+ body: z.string().min(1),
57
+ })
58
+ )
59
+ .mutation(async ({ ctx, input }) => {
60
+ await ctx.db.comment.create({ data: { articleId: input.articleId } });
61
+ }),
62
+ list: publicProcedure.query(async ({ ctx }) => ctx.db.comment.findMany()),
63
+ });`;
64
+ const ROUTER_WITH_BARE_MUTATION = `
65
+ export const appRouter = t.router({
66
+ deleteAll: publicProcedure.mutation(async ({ ctx, input }) => {
67
+ await ctx.prisma.post.deleteMany();
68
+ }),
69
+ });`;
70
+ (0, vitest_1.describe)("trpc extractProcedures — 括号感知链", () => {
71
+ (0, vitest_1.it)("标准多行 .input(z.object({...})) mutation 可见且有 input schema(V4 缺陷回归)", () => {
72
+ const procs = (0, trpc_detector_1.extractProcedures)(ROUTER_WITH_INPUT);
73
+ const add = procs.find((p) => p.name === "addComment");
74
+ (0, vitest_1.expect)(add).toBeDefined();
75
+ (0, vitest_1.expect)(add.kind).toBe("mutation");
76
+ (0, vitest_1.expect)(add.procedureType).toBe("protected");
77
+ (0, vitest_1.expect)(add.hasInputSchema).toBe(true);
78
+ // 无 schema 的 query 也照常可见
79
+ (0, vitest_1.expect)(procs.some((p) => p.name === "list" && p.hasInputSchema === false)).toBe(true);
80
+ });
81
+ (0, vitest_1.it)("单行 .input(z.string()) 链可见", () => {
82
+ const procs = (0, trpc_detector_1.extractProcedures)(`
83
+ export const r = t.router({
84
+ getOne: protectedProcedure.input(z.string()).query(async ({ ctx, input }) => {
85
+ return ctx.db.get(input);
86
+ }),
87
+ });`);
88
+ const p = procs.find((x) => x.name === "getOne");
89
+ (0, vitest_1.expect)(p).toBeDefined();
90
+ (0, vitest_1.expect)(p.hasInputSchema).toBe(true);
91
+ });
92
+ (0, vitest_1.it)("裸链 mutation(无 input)仍可见并可触发规则", () => {
93
+ const procs = (0, trpc_detector_1.extractProcedures)(ROUTER_WITH_BARE_MUTATION);
94
+ const del = procs.find((p) => p.name === "deleteAll");
95
+ (0, vitest_1.expect)(del).toBeDefined();
96
+ (0, vitest_1.expect)(del.kind).toBe("mutation");
97
+ (0, vitest_1.expect)(del.hasInputSchema).toBe(false);
98
+ (0, vitest_1.expect)(del.doesDbWrite).toBe(true);
99
+ });
100
+ (0, vitest_1.it)("完整分析:合规 router 0 issues,裸 public mutation 报 TRPC_PUBLIC_MUTATION", () => {
101
+ const tmp = path.join(os.tmpdir(), "trpc-good-router.ts");
102
+ fs.writeFileSync(tmp, ROUTER_WITH_INPUT);
103
+ try {
104
+ const good = (0, trpc_detector_1.analyzeTRPCFile)(tmp);
105
+ (0, vitest_1.expect)(good.issues).toHaveLength(0);
106
+ (0, vitest_1.expect)(good.procedures.length).toBe(2);
107
+ }
108
+ finally {
109
+ fs.unlinkSync(tmp);
110
+ }
111
+ const tmp2 = path.join(os.tmpdir(), "trpc-bad-router.ts");
112
+ fs.writeFileSync(tmp2, ROUTER_WITH_BARE_MUTATION);
113
+ try {
114
+ const bad = (0, trpc_detector_1.analyzeTRPCFile)(tmp2);
115
+ (0, vitest_1.expect)(bad.issues.map((i) => i.rule)).toContain("TRPC_PUBLIC_MUTATION");
116
+ (0, vitest_1.expect)(bad.issues.map((i) => i.rule)).toContain("TRPC_MUTATION_WITHOUT_INPUT_SCHEMA");
117
+ }
118
+ finally {
119
+ fs.unlinkSync(tmp2);
120
+ }
121
+ });
122
+ });
123
+ // ── 修复 2:lastIndex 泄漏回归 ──
124
+ (0, vitest_1.describe)("detectTRPCApp — 无 /g lastIndex 泄漏", () => {
125
+ (0, vitest_1.it)("连续多次调用结果稳定(旧 /g 实现会漂移)", () => {
126
+ const trpcCode = `const t = initTRPC.create(); export const r = t.router({ a: publicProcedure.query(() => 1) });`;
127
+ const plainCode = `export const sum = (a: number, b: number) => a + b;`;
128
+ // 交替调用多次:泄漏时第二次起结果不稳定
129
+ const results = [];
130
+ for (let i = 0; i < 6; i++) {
131
+ results.push((0, trpc_detector_1.detectTRPCApp)(trpcCode)); // 应为 true
132
+ results.push((0, trpc_detector_1.detectTRPCApp)(plainCode)); // 应为 false
133
+ }
134
+ (0, vitest_1.expect)(results.filter(Boolean)).toHaveLength(6); // 恰好 6 个 true
135
+ (0, vitest_1.expect)(results).toEqual([
136
+ true, false, true, false, true, false, true, false, true, false, true, false,
137
+ ]);
138
+ });
139
+ });
140
+ // ── tRPC v11:内联 t.procedure 形态(V4 遗留缺口)──
141
+ (0, vitest_1.describe)("trpc v11 t.procedure 支持", () => {
142
+ (0, vitest_1.it)("t.procedure.input(z.object).mutation 可见且视为公开(默认语义)", () => {
143
+ const procs = (0, trpc_detector_1.extractProcedures)(`
144
+ import { initTRPC } from "@trpc/server";
145
+ const t = initTRPC.create();
146
+ export const r = t.router({
147
+ ping: t.procedure
148
+ .input(z.object({ msg: z.string() }))
149
+ .mutation(async ({ ctx, input }) => {
150
+ await ctx.prisma.log.create({ data: { msg: input.msg } });
151
+ }),
152
+ list: t.procedure.query(async () => []),
153
+ });`);
154
+ const ping = procs.find((p) => p.name === "ping");
155
+ (0, vitest_1.expect)(ping).toBeDefined();
156
+ (0, vitest_1.expect)(ping.kind).toBe("mutation");
157
+ (0, vitest_1.expect)(ping.procedureType).toBe("public");
158
+ (0, vitest_1.expect)(ping.hasInputSchema).toBe(true);
159
+ (0, vitest_1.expect)(procs.some((p) => p.name === "list")).toBe(true);
160
+ });
161
+ (0, vitest_1.it)("裸 t.procedure mutation(无 input)触发规则(敏感性与命名包装一致)", () => {
162
+ const procs = (0, trpc_detector_1.extractProcedures)(`
163
+ export const r = t.router({
164
+ nuke: t.procedure.mutation(async ({ ctx }) => {
165
+ await ctx.prisma.post.deleteMany();
166
+ }),
167
+ });`);
168
+ const nuke = procs.find((p) => p.name === "nuke");
169
+ (0, vitest_1.expect)(nuke).toBeDefined();
170
+ (0, vitest_1.expect)(nuke.hasInputSchema).toBe(false);
171
+ (0, vitest_1.expect)(nuke.doesDbWrite).toBe(true);
172
+ });
173
+ (0, vitest_1.it)("netflx v10 命名包装形态不受影响(19/19 保持)", () => {
174
+ const procs = (0, trpc_detector_1.extractProcedures)(ROUTER_WITH_INPUT);
175
+ (0, vitest_1.expect)(procs.length).toBe(2);
176
+ });
177
+ });
package/dist/sdk.js CHANGED
@@ -20,7 +20,7 @@ const risk_model_1 = require("./risk-model");
20
20
  const protocol_knowledge_1 = require("./protocol-knowledge");
21
21
  const evidence_repository_1 = require("./evidence-repository");
22
22
  /** Runtime version — stable public identifier. Internal layers evolve underneath. */
23
- exports.RUNTIME_VERSION = "3.7.14";
23
+ exports.RUNTIME_VERSION = "3.7.16";
24
24
  function verify(filePath) {
25
25
  const cert = (0, certify_1.certify)(filePath);
26
26
  const kb = (0, protocol_knowledge_1.buildKnowledgeBase)();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "progmune-runtime",
3
- "version": "3.7.14",
3
+ "version": "3.7.16",
4
4
  "description": "Progmune — AI Trust Decision Engine. Verify AI-generated code before it reaches production. Outputs APPROVED / NEEDS_REVIEW / BLOCKED with evidence.",
5
5
  "files": [
6
6
  "dist/",
@@ -88,7 +88,8 @@
88
88
  "test:soak": "vitest run tests/soak/",
89
89
  "test:p7": "vitest run tests/p7-*/",
90
90
  "fetch:cves": "tsx scripts/fetch-cves.ts",
91
- "test:cve": "vitest run src/cve-benchmark.test.ts"
91
+ "test:cve": "vitest run src/cve-benchmark.test.ts",
92
+ "audit:realworld": "node scripts/realworld-audit.js"
92
93
  },
93
94
  "keywords": [
94
95
  "program-synthesis",
@@ -114,4 +115,4 @@
114
115
  "tsx": "^4.22.4",
115
116
  "vitest": "^3.2.6"
116
117
  }
117
- }
118
+ }
@@ -161,14 +161,68 @@ class UrlCollector(ast.NodeVisitor):
161
161
  def __init__(self, filepath):
162
162
  self.file = filepath
163
163
  self.routes = []
164
+ # DRF ViewSet/DefaultRouter(REALWORLD_STRUCTURAL_V3:router.register
165
+ # + include(router.urls) 的路由由运行时生成,urlpatterns 解析不可见)
166
+ self.router_regs = {} # router 变量 -> [(prefix, viewName)]
167
+ self.router_slash = {} # router 变量 -> trailing_slash
168
+ self.router_uses = [] # (prefixBase, routerVar) urlpatterns 里的 include
164
169
 
165
170
  def visit_Assign(self, node):
166
171
  for t in node.targets:
167
- if isinstance(t, ast.Name) and t.id == "urlpatterns" and isinstance(node.value, (ast.List, ast.Tuple)):
172
+ if not isinstance(t, ast.Name):
173
+ continue
174
+ # DefaultRouter 定义(trailing_slash 关键字,默认 True)
175
+ if isinstance(node.value, ast.Call) and isinstance(node.value.func, ast.Name) \
176
+ and node.value.func.id == "DefaultRouter":
177
+ slash = True
178
+ for kw in node.value.keywords:
179
+ if kw.arg == "trailing_slash" and isinstance(kw.value, ast.Constant):
180
+ slash = bool(kw.value.value)
181
+ self.router_slash[t.id] = slash
182
+ self.router_regs.setdefault(t.id, [])
183
+ # urlpatterns:扫描直连条目,并展开 include(router.urls) 的 ViewSet
184
+ if t.id == "urlpatterns" and isinstance(node.value, (ast.List, ast.Tuple)):
168
185
  for elt in node.value.elts:
169
186
  self._scan_entry(elt)
187
+ for (prefix, var) in self.router_uses:
188
+ self._expand_router_routes(prefix, var)
170
189
  self.generic_visit(node)
171
190
 
191
+ def visit_Expr(self, node):
192
+ # router.register(r'articles', ArticleViewSet, ...) 语句
193
+ call = node.value
194
+ if isinstance(call, ast.Call) and isinstance(call.func, ast.Attribute) \
195
+ and call.func.attr == "register":
196
+ var = name_of(call.func.value)
197
+ if not var or var not in self.router_regs:
198
+ return
199
+ prefix = ""
200
+ if call.args and isinstance(call.args[0], ast.Constant):
201
+ prefix = str(call.args[0].value)
202
+ view = None
203
+ if len(call.args) > 1:
204
+ a1 = call.args[1]
205
+ if isinstance(a1, ast.Name):
206
+ view = a1.id
207
+ elif isinstance(a1, ast.Attribute):
208
+ view = a1.attr
209
+ if view:
210
+ self.router_regs.setdefault(var, []).append((prefix, view))
211
+ self.generic_visit(node)
212
+
213
+ def _expand_router_routes(self, prefix_base, var):
214
+ # 本工具不做 include 前缀传播(urlconf 各文件 pattern 独立)——
215
+ # prefix_base 仅作路由存在性触发,不拼进 pattern
216
+ for (prefix, view) in self.router_regs.get(var, []):
217
+ base = "^" + prefix
218
+ # DRF 默认 action 路由:list/create 集合级、retrieve/update/
219
+ # destroy 详情级(权限由 views 表判定,规则在 TS 检测器侧)
220
+ for pat in (base + "/?$", base + "/(?P<pk>[^/.]+)/?$"):
221
+ self.routes.append({
222
+ "pattern": pat, "urlname": "", "view": view, "kind": "cbv",
223
+ "file": self.file, "viewset": True,
224
+ })
225
+
172
226
  def _scan_entry(self, elt):
173
227
  if not isinstance(elt, ast.Call):
174
228
  return
@@ -199,6 +253,13 @@ class UrlCollector(ast.NodeVisitor):
199
253
  elif isinstance(view_ref, ast.Call) and isinstance(view_ref.func, ast.Name):
200
254
  if view_ref.func.id == "include":
201
255
  kind = "include"
256
+ # include(router.urls):记录 router 变量(同一文件的
257
+ # router.register 展开见 _expand_router_routes)
258
+ if view_ref.args and isinstance(view_ref.args[0], ast.Attribute) \
259
+ and view_ref.args[0].attr == "urls":
260
+ rvar = name_of(view_ref.args[0].value)
261
+ if rvar:
262
+ self.router_uses.append((pattern, rvar))
202
263
  else:
203
264
  kind = "fbv"
204
265
  view_name = view_ref.func.id
@@ -26,8 +26,10 @@ SKIP_DIRS = {"tests", "test", "deps", "venv", "env", "node_modules", "vendor",
26
26
  ".git", "migrations", "__pycache__", "scripts", "docs",
27
27
  "staticfiles", "static"}
28
28
 
29
+ # jwt 必在词表:flask_jwt_extended 的 @jwt_required 是生态头号认证装饰器
30
+ # (REALWORLD_STRUCTURAL_V4:缺 "jwt" → 10 个受保护 mutation 全误报 FP)
29
31
  AUTH_WORDS = ("auth", "login", "permission", "token", "credential", "session",
30
- "user")
32
+ "user", "jwt")
31
33
 
32
34
  MUTATION_METHODS = {"POST", "PUT", "PATCH", "DELETE"}
33
35
 
@@ -30,9 +30,13 @@ import sys
30
30
 
31
31
  # 认证词表(依赖函数名命中即视为 auth-like;与 TS 侧 annotation-suggest 词表同源)
32
32
  # 注意:扫描器只做「结构提取」,规则判定(豁免词/方法门控)在 TS 检测器侧。
33
+ # 注意:不含裸 "user"——get_profile_by_username_from_path 等 DB 查询
34
+ # 依赖名含 user 会被误标认证(REALWORLD_STRUCTURAL_V2 假保护 FN)。
35
+ # 真认证依赖用 current_user/authorizer 等强词识别(get_current_user
36
+ # 含 current_user ✓)。
33
37
  AUTH_WORDS = (
34
- "auth", "login", "token", "user", "credential", "session",
35
- "bearer", "permission", "current_user", "api_key", "oauth",
38
+ "auth", "login", "token", "credential", "session",
39
+ "bearer", "permission", "current_user", "api_key", "oauth", "jwt",
36
40
  )
37
41
 
38
42
  SKIP_DIRS = {"tests", "test", "deps", "venv", "env", "node_modules", "vendor",