@securecode-ai/mcp 0.1.0

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.
Files changed (103) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +71 -0
  3. package/dist/api/client.d.ts +22 -0
  4. package/dist/api/client.js +141 -0
  5. package/dist/api/client.js.map +1 -0
  6. package/dist/api/types.d.ts +129 -0
  7. package/dist/api/types.js +3 -0
  8. package/dist/api/types.js.map +1 -0
  9. package/dist/approval/auditLog.d.ts +3 -0
  10. package/dist/approval/auditLog.js +67 -0
  11. package/dist/approval/auditLog.js.map +1 -0
  12. package/dist/approval/broker.d.ts +11 -0
  13. package/dist/approval/broker.js +252 -0
  14. package/dist/approval/broker.js.map +1 -0
  15. package/dist/approval/types.d.ts +26 -0
  16. package/dist/approval/types.js +58 -0
  17. package/dist/approval/types.js.map +1 -0
  18. package/dist/attack/agentLoop.d.ts +21 -0
  19. package/dist/attack/agentLoop.js +148 -0
  20. package/dist/attack/agentLoop.js.map +1 -0
  21. package/dist/attack/executor.d.ts +17 -0
  22. package/dist/attack/executor.js +162 -0
  23. package/dist/attack/executor.js.map +1 -0
  24. package/dist/attack/policy.d.ts +26 -0
  25. package/dist/attack/policy.js +73 -0
  26. package/dist/attack/policy.js.map +1 -0
  27. package/dist/attack/protocol.d.ts +64 -0
  28. package/dist/attack/protocol.js +3 -0
  29. package/dist/attack/protocol.js.map +1 -0
  30. package/dist/attack/report.d.ts +16 -0
  31. package/dist/attack/report.js +54 -0
  32. package/dist/attack/report.js.map +1 -0
  33. package/dist/auth/credentialStore.d.ts +20 -0
  34. package/dist/auth/credentialStore.js +148 -0
  35. package/dist/auth/credentialStore.js.map +1 -0
  36. package/dist/auth/keychain.d.ts +14 -0
  37. package/dist/auth/keychain.js +267 -0
  38. package/dist/auth/keychain.js.map +1 -0
  39. package/dist/index.d.ts +2 -0
  40. package/dist/index.js +170 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/mcp/server.d.ts +2 -0
  43. package/dist/mcp/server.js +183 -0
  44. package/dist/mcp/server.js.map +1 -0
  45. package/dist/mcp/tools.d.ts +2 -0
  46. package/dist/mcp/tools.js +120 -0
  47. package/dist/mcp/tools.js.map +1 -0
  48. package/dist/mcp/types.d.ts +26 -0
  49. package/dist/mcp/types.js +3 -0
  50. package/dist/mcp/types.js.map +1 -0
  51. package/dist/project-map/astHelpers.d.ts +89 -0
  52. package/dist/project-map/astHelpers.js +468 -0
  53. package/dist/project-map/astHelpers.js.map +1 -0
  54. package/dist/project-map/cache.d.ts +11 -0
  55. package/dist/project-map/cache.js +86 -0
  56. package/dist/project-map/cache.js.map +1 -0
  57. package/dist/project-map/confidence.d.ts +50 -0
  58. package/dist/project-map/confidence.js +87 -0
  59. package/dist/project-map/confidence.js.map +1 -0
  60. package/dist/project-map/crossFile.d.ts +73 -0
  61. package/dist/project-map/crossFile.js +33 -0
  62. package/dist/project-map/crossFile.js.map +1 -0
  63. package/dist/project-map/detectors.d.ts +83 -0
  64. package/dist/project-map/detectors.js +376 -0
  65. package/dist/project-map/detectors.js.map +1 -0
  66. package/dist/project-map/layer1.d.ts +74 -0
  67. package/dist/project-map/layer1.js +928 -0
  68. package/dist/project-map/layer1.js.map +1 -0
  69. package/dist/project-map/layer2.d.ts +30 -0
  70. package/dist/project-map/layer2.js +249 -0
  71. package/dist/project-map/layer2.js.map +1 -0
  72. package/dist/project-map/mapBuilder.d.ts +17 -0
  73. package/dist/project-map/mapBuilder.js +164 -0
  74. package/dist/project-map/mapBuilder.js.map +1 -0
  75. package/dist/project-map/parserLoader.d.ts +61 -0
  76. package/dist/project-map/parserLoader.js +175 -0
  77. package/dist/project-map/parserLoader.js.map +1 -0
  78. package/dist/project-map/schemas.d.ts +28 -0
  79. package/dist/project-map/schemas.js +131 -0
  80. package/dist/project-map/schemas.js.map +1 -0
  81. package/dist/project-map/types.d.ts +232 -0
  82. package/dist/project-map/types.js +61 -0
  83. package/dist/project-map/types.js.map +1 -0
  84. package/dist/tools/attack.d.ts +2 -0
  85. package/dist/tools/attack.js +80 -0
  86. package/dist/tools/attack.js.map +1 -0
  87. package/dist/tools/fix.d.ts +2 -0
  88. package/dist/tools/fix.js +60 -0
  89. package/dist/tools/fix.js.map +1 -0
  90. package/dist/tools/map.d.ts +2 -0
  91. package/dist/tools/map.js +46 -0
  92. package/dist/tools/map.js.map +1 -0
  93. package/dist/tools/scan.d.ts +2 -0
  94. package/dist/tools/scan.js +55 -0
  95. package/dist/tools/scan.js.map +1 -0
  96. package/dist/utils/files.d.ts +8 -0
  97. package/dist/utils/files.js +79 -0
  98. package/dist/utils/files.js.map +1 -0
  99. package/package.json +53 -0
  100. package/resources/tree-sitter/tree-sitter-javascript.wasm +0 -0
  101. package/resources/tree-sitter/tree-sitter-python.wasm +0 -0
  102. package/resources/tree-sitter/tree-sitter-tsx.wasm +0 -0
  103. package/resources/tree-sitter/tree-sitter-typescript.wasm +0 -0
@@ -0,0 +1,928 @@
1
+ "use strict";
2
+ /**
3
+ * Phase 4 Layer 1 — static extraction.
4
+ *
5
+ * Walks a parsed file once and produces one EndpointRecord per route
6
+ * registration site, plus the cross-file context (middleware chain,
7
+ * validators, params, auth, ORM, call graph, response shape).
8
+ *
9
+ * Four registration styles are recognised, because they are the four ways the
10
+ * corpus of real projects actually declares routes:
11
+ *
12
+ * - a call on a router object `app.get('/x', handler)` / `@app.get('/x')`
13
+ * - a filesystem path Next.js `app/api/x/route.ts` exporting GET
14
+ * - a URLconf table Django `path('x/', views.x)`
15
+ *
16
+ * Everything that needs to look outside this file — which middleware the
17
+ * router is mounted behind, what a middleware module actually checks, whether
18
+ * an imported singleton is a Prisma client — arrives through the optional
19
+ * `CrossFileContext`, which the cache builds (it owns the filesystem). Layer 1
20
+ * itself stays a pure function of nodes and text.
21
+ */
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.findFunctionByName = findFunctionByName;
24
+ exports.resolveRoutePath = resolveRoutePath;
25
+ exports.nextAppRouterPath = nextAppRouterPath;
26
+ exports.extractLayer1 = extractLayer1;
27
+ const parserLoader_1 = require("./parserLoader");
28
+ const astHelpers_1 = require("./astHelpers");
29
+ const crossFile_1 = require("./crossFile");
30
+ const schemas_1 = require("./schemas");
31
+ const detectors_1 = require("./detectors");
32
+ const types_1 = require("./types");
33
+ // ---------------------------------------------------------------------------
34
+ // Parameters
35
+ // ---------------------------------------------------------------------------
36
+ /** Infer the ParamSource from a member like `req.body.x` / `req.query.y`. */
37
+ function paramSourceFromAccess(txt) {
38
+ if (/\bbody\b/.test(txt))
39
+ return 'body';
40
+ if (/\bquery\b/.test(txt))
41
+ return 'query';
42
+ if (/\bparams\b/.test(txt))
43
+ return 'path';
44
+ if (/\bheaders?\b/.test(txt))
45
+ return 'header';
46
+ if (/\bcookies?\b/.test(txt))
47
+ return 'cookie';
48
+ return 'unknown';
49
+ }
50
+ /** Accessor forms that name their own parameter in a string argument. */
51
+ const NAMED_ACCESSORS = [
52
+ // Next.js: req.nextUrl.searchParams.get('q'), new URL(...).searchParams
53
+ { re: /\bsearchParams\s*\.\s*get\(\s*['"`]([^'"`]+)['"`]/g, source: 'query' },
54
+ // Django: request.GET.get("tag") / request.GET["tag"]
55
+ { re: /\brequest\s*\.\s*GET\s*\.\s*get\(\s*['"]([^'"]+)['"]/g, source: 'query' },
56
+ { re: /\brequest\s*\.\s*GET\s*\[\s*['"]([^'"]+)['"]\s*\]/g, source: 'query' },
57
+ { re: /\brequest\s*\.\s*POST\s*\.\s*get\(\s*['"]([^'"]+)['"]/g, source: 'body' },
58
+ { re: /\brequest\s*\.\s*POST\s*\[\s*['"]([^'"]+)['"]\s*\]/g, source: 'body' },
59
+ // Express bracket access: req.query['page'], req.headers['x-api-key']
60
+ { re: /\b(?:req|request)\s*\.\s*query\s*\[\s*['"]([^'"]+)['"]\s*\]/g, source: 'query' },
61
+ { re: /\b(?:req|request)\s*\.\s*body\s*\[\s*['"]([^'"]+)['"]\s*\]/g, source: 'body' },
62
+ { re: /\b(?:req|request)\s*\.\s*params\s*\[\s*['"]([^'"]+)['"]\s*\]/g, source: 'path' },
63
+ { re: /\b(?:req|request)\s*\.\s*(?:headers|header)\s*\[\s*['"]([^'"]+)['"]\s*\]/g, source: 'header' },
64
+ ];
65
+ /** Request objects a destructuring can pull a whole parameter bag out of. */
66
+ const DESTRUCTURE_SOURCE_RE = /^(?:req|request|ctx)\.(body|query|params|headers|cookies)$/;
67
+ /**
68
+ * Extract the parameters an endpoint consumes.
69
+ *
70
+ * Reading `req.<source>.<name>` is only one of the ways a handler names its
71
+ * inputs, and on real code it is the minority: bodies are commonly
72
+ * destructured or declared entirely inside a validation schema, path params
73
+ * arrive through a framework context object, and FastAPI declares everything
74
+ * in the signature. All of those forms are collected here.
75
+ */
76
+ function extractParams(handler, seed, framework) {
77
+ const out = new Map();
78
+ const add = (p) => {
79
+ const existing = out.get(p.name);
80
+ if (!existing) {
81
+ out.set(p.name, p);
82
+ return;
83
+ }
84
+ // A field covered by a schema is validated even if it is also read raw.
85
+ if (!existing.validated && p.validated) {
86
+ out.set(p.name, { ...existing, validated: true, validator: p.validator });
87
+ }
88
+ };
89
+ for (const p of seed)
90
+ add(p);
91
+ const { node, source, schemas } = handler;
92
+ const text = source.slice(node.startIndex, node.endIndex);
93
+ // --- 1. member reads: req.body.x / req.params.id -----------------------
94
+ for (const n of (0, astHelpers_1.walk)(node)) {
95
+ if (n.type !== 'member_expression' && n.type !== 'attribute' && n.type !== 'subscript')
96
+ continue;
97
+ const txt = source.slice(n.startIndex, n.endIndex);
98
+ const m = txt.match(/\b(req|request|ctx|c)\.(body|query|params|headers|header|cookies)\.(\w+)/i);
99
+ if (!m)
100
+ continue;
101
+ add({
102
+ name: m[3],
103
+ type: 'unknown',
104
+ source: paramSourceFromAccess(txt),
105
+ validated: false,
106
+ confidence: types_1.Confidence.STATIC_LITERAL,
107
+ });
108
+ }
109
+ // --- 2. destructuring: const { qty } = req.body ------------------------
110
+ for (const n of (0, astHelpers_1.walk)(node)) {
111
+ if (n.type !== 'variable_declarator' && n.type !== 'assignment')
112
+ continue;
113
+ const target = n.child(0);
114
+ const value = n.child(n.childCount - 1);
115
+ if (!target || !value || target === value)
116
+ continue;
117
+ if (target.type !== 'object_pattern' && target.type !== 'object')
118
+ continue;
119
+ const valueText = source.slice(value.startIndex, value.endIndex).trim();
120
+ const m = valueText.match(DESTRUCTURE_SOURCE_RE);
121
+ if (!m)
122
+ continue;
123
+ const src = paramSourceFromAccess(m[1]);
124
+ for (const name of patternBindings(target, source)) {
125
+ add({
126
+ name,
127
+ type: 'unknown',
128
+ source: src,
129
+ validated: false,
130
+ confidence: types_1.Confidence.STATIC_LITERAL,
131
+ });
132
+ }
133
+ }
134
+ // --- 3. accessors that name the parameter in a string ------------------
135
+ for (const { re, source: src } of NAMED_ACCESSORS) {
136
+ re.lastIndex = 0;
137
+ let m;
138
+ while ((m = re.exec(text)) !== null) {
139
+ add({
140
+ name: m[1],
141
+ type: 'unknown',
142
+ source: src,
143
+ validated: false,
144
+ confidence: types_1.Confidence.STATIC_LITERAL,
145
+ });
146
+ }
147
+ }
148
+ // --- 4. Python signature declarations ----------------------------------
149
+ for (const p of signatureParameters(node)) {
150
+ const txt = source.slice(p.startIndex, p.endIndex);
151
+ const nameNode = p.child(0);
152
+ if (!nameNode || !(0, astHelpers_1.isIdentifier)(nameNode))
153
+ continue;
154
+ const name = source.slice(nameNode.startIndex, nameNode.endIndex);
155
+ // A parameter annotated with a schema model IS the body: its fields,
156
+ // not the parameter name, are what the request carries.
157
+ const annotation = txt.split(':')[1]?.split('=')[0]?.trim();
158
+ const model = annotation ? schemas.get(annotation) : undefined;
159
+ if (model) {
160
+ for (const field of model.fields) {
161
+ add({
162
+ name: field,
163
+ type: 'unknown',
164
+ source: 'body',
165
+ validated: true,
166
+ validator: model.library,
167
+ confidence: types_1.Confidence.STATIC_LITERAL,
168
+ });
169
+ }
170
+ continue;
171
+ }
172
+ let src = 'unknown';
173
+ if (/\bPath\s*\(/.test(txt))
174
+ src = 'path';
175
+ else if (/\bQuery\s*\(/.test(txt))
176
+ src = 'query';
177
+ else if (/\bBody\s*\(/.test(txt))
178
+ src = 'body';
179
+ else if (/\bHeader\s*\(/.test(txt))
180
+ src = 'header';
181
+ else if (/\bCookie\s*\(/.test(txt))
182
+ src = 'cookie';
183
+ if (src === 'unknown')
184
+ continue;
185
+ add({
186
+ name,
187
+ type: annotation || 'unknown',
188
+ source: src,
189
+ // FastAPI's Path()/Query()/Body() coerce and reject at the framework
190
+ // level, which is validation even without a validator library.
191
+ validated: true,
192
+ validator: 'pydantic',
193
+ confidence: types_1.Confidence.STATIC_LITERAL,
194
+ });
195
+ }
196
+ // --- 5. schema validation calls: CreateUser.parse(req.body) ------------
197
+ for (const n of (0, astHelpers_1.walk)(node)) {
198
+ if (n.type !== 'call_expression' && n.type !== 'call')
199
+ continue;
200
+ const p = (0, astHelpers_1.callParts)(n, source);
201
+ if (!p || !p.receiver)
202
+ continue;
203
+ if (!schemas_1.SCHEMA_VALIDATION_METHODS.has(p.method))
204
+ continue;
205
+ const schema = schemas.get(p.receiver);
206
+ if (!schema)
207
+ continue;
208
+ for (const field of schema.fields) {
209
+ add({
210
+ name: field,
211
+ type: 'unknown',
212
+ source: 'body',
213
+ validated: true,
214
+ validator: schema.library,
215
+ confidence: types_1.Confidence.STATIC_LITERAL,
216
+ });
217
+ }
218
+ }
219
+ // Next.js path params arrive on the context object, which the folder name
220
+ // already told us about; nothing further to read from the body.
221
+ void framework;
222
+ return [...out.values()];
223
+ }
224
+ /** Names bound by an object/array destructuring pattern. */
225
+ function patternBindings(pattern, source) {
226
+ const names = [];
227
+ for (const n of (0, astHelpers_1.walk)(pattern)) {
228
+ if (n.type === 'shorthand_property_identifier_pattern'
229
+ || n.type === 'shorthand_property_identifier') {
230
+ names.push(source.slice(n.startIndex, n.endIndex));
231
+ continue;
232
+ }
233
+ if (n.type === 'pair_pattern' || n.type === 'pair') {
234
+ const value = n.namedChildren[n.namedChildren.length - 1];
235
+ if (value && (0, astHelpers_1.isIdentifier)(value)) {
236
+ names.push(source.slice(value.startIndex, value.endIndex));
237
+ }
238
+ }
239
+ }
240
+ return names;
241
+ }
242
+ /** The parameter nodes of a function definition, if it has a signature. */
243
+ function signatureParameters(fn) {
244
+ const params = fn.namedChildren.find(c => c.type === 'parameters' || c.type === 'formal_parameters');
245
+ if (!params)
246
+ return [];
247
+ return params.namedChildren.filter(p => p.type === 'typed_parameter' || p.type === 'typed_default_parameter');
248
+ }
249
+ // ---------------------------------------------------------------------------
250
+ // Call graph
251
+ // ---------------------------------------------------------------------------
252
+ /** Receivers that build the response rather than doing work the handler cares about. */
253
+ const RESPONSE_RECEIVERS = new Set([
254
+ 'res', 'response', 'reply', 'ctx',
255
+ 'NextResponse', 'JsonResponse', 'HttpResponse', 'HttpResponseRedirect',
256
+ ]);
257
+ /** Free functions that construct a response (Django `render`, Flask `jsonify`). */
258
+ const RESPONSE_BUILDERS = new Set(Object.keys(detectors_1.RESPONSE_CONSTRUCTORS));
259
+ /** Build the call graph for a handler: every function it calls. */
260
+ function extractCallGraph(handlerRoot, source, imports) {
261
+ const seen = new Set();
262
+ const out = [];
263
+ // Framework declarations in the handler's own SIGNATURE — FastAPI's
264
+ // `Depends()` / `Query()` / `Path()` / `Body()` — describe how the request
265
+ // is bound, not work the handler does, and are already reflected in the
266
+ // params and authScheme. Their span is captured up front because
267
+ // tree-sitter hands back a fresh wrapper object on every `.parent` access,
268
+ // so ancestors cannot be compared by identity.
269
+ const signature = handlerRoot.namedChildren.find(c => c.type === 'parameters' || c.type === 'formal_parameters');
270
+ const inSignature = (n) => Boolean(signature)
271
+ && n.startIndex >= signature.startIndex
272
+ && n.endIndex <= signature.endIndex;
273
+ for (const n of (0, astHelpers_1.walk)(handlerRoot)) {
274
+ if (n.type !== 'call_expression' && n.type !== 'call')
275
+ continue;
276
+ const p = (0, astHelpers_1.callParts)(n, source);
277
+ if (!p)
278
+ continue;
279
+ // Skip res.json/send/etc. — those are response shape, not call graph.
280
+ if (p.receiver && RESPONSE_RECEIVERS.has(p.receiver))
281
+ continue;
282
+ if (!p.receiver && RESPONSE_BUILDERS.has(p.method))
283
+ continue;
284
+ // Skip the framework's own registration methods (don't recurse into app.get).
285
+ if (p.receiver === 'app' || p.receiver === 'router' || p.receiver === 'fastify' || p.receiver === 'server')
286
+ continue;
287
+ if (inSignature(n))
288
+ continue;
289
+ const name = p.receiver ? `${p.receiver}.${p.method}` : p.method;
290
+ if (seen.has(name))
291
+ continue;
292
+ seen.add(name);
293
+ const calleeFile = imports.get(p.receiver || p.method) || imports.get(p.method);
294
+ // A call written out in the source always happens; the only thing the
295
+ // import map adds is WHERE the callee is defined. Downgrading a call we
296
+ // can see to 0.6 because we cannot name its file conflates "does this
297
+ // call happen" with "which file defines it".
298
+ out.push({
299
+ name,
300
+ calleeFile,
301
+ callLine: p.line + 1,
302
+ confidence: types_1.Confidence.STATIC_LITERAL,
303
+ });
304
+ }
305
+ return out;
306
+ }
307
+ // ---------------------------------------------------------------------------
308
+ // Handler lookup
309
+ // ---------------------------------------------------------------------------
310
+ /** Get the name of a handler function node (or '<anonymous>'). */
311
+ function handlerNameOf(node, source) {
312
+ if (!node)
313
+ return '<anonymous>';
314
+ // function foo() {}
315
+ const id = node.namedChildren.find(c => (0, astHelpers_1.isIdentifier)(c));
316
+ if (id)
317
+ return source.slice(id.startIndex, id.endIndex);
318
+ // const foo = (...) => {}
319
+ const parent = node.parent;
320
+ if (parent && (parent.type === 'lexical_declaration' || parent.type === 'variable_declaration' || parent.type === 'assignment_expression')) {
321
+ const name = parent.child(0);
322
+ if (name && (0, astHelpers_1.isIdentifier)(name))
323
+ return source.slice(name.startIndex, name.endIndex);
324
+ }
325
+ return '<anonymous>';
326
+ }
327
+ /** Locate a top-level (or nested) function declaration by name. */
328
+ function findFunctionByName(root, source, name) {
329
+ for (const n of (0, astHelpers_1.walk)(root)) {
330
+ if (n.type === 'function_declaration' || n.type === 'function_definition' || n.type === 'method_definition') {
331
+ const id = n.namedChildren.find(c => (0, astHelpers_1.isIdentifier)(c));
332
+ if (id && source.slice(id.startIndex, id.endIndex) === name)
333
+ return n;
334
+ }
335
+ // const name = (...) => {}
336
+ if (n.type === 'lexical_declaration' || n.type === 'variable_declaration') {
337
+ for (const decl of n.namedChildren) {
338
+ if (decl.type !== 'variable_declarator')
339
+ continue;
340
+ const declName = decl.child(0);
341
+ if (declName && (0, astHelpers_1.isIdentifier)(declName) && source.slice(declName.startIndex, declName.endIndex) === name) {
342
+ const fnNode = decl.child(decl.childCount - 1);
343
+ if (fnNode && (fnNode.type === 'arrow_function' || fnNode.type === 'function_expression')) {
344
+ return fnNode;
345
+ }
346
+ }
347
+ }
348
+ }
349
+ }
350
+ return null;
351
+ }
352
+ /**
353
+ * The Python function a decorator is attached to, for `@app.get("/x")` route
354
+ * decorators. Returns null for any other call node.
355
+ */
356
+ function decoratedFunction(callNode) {
357
+ const decorator = callNode.parent;
358
+ if (!decorator || decorator.type !== 'decorator')
359
+ return null;
360
+ const owner = decorator.parent;
361
+ if (!owner || owner.type !== 'decorated_definition')
362
+ return null;
363
+ return owner.namedChildren.find(c => c.type === 'function_definition' || c.type === 'async_function_definition') ?? null;
364
+ }
365
+ /** Decorator source text applied to a Python function definition. */
366
+ function decoratorsOf(fn, source) {
367
+ const owner = fn.parent;
368
+ if (!owner || owner.type !== 'decorated_definition')
369
+ return [];
370
+ return owner.namedChildren
371
+ .filter(c => c.type === 'decorator')
372
+ .map(c => source.slice(c.startIndex, c.endIndex));
373
+ }
374
+ /**
375
+ * Resolve the first argument of a route registration to a path.
376
+ *
377
+ * A string literal is taken as written. A bare identifier is resolved against
378
+ * the file's string assignments (`const BASE = '/v2'`) at intra-file-dataflow
379
+ * confidence. A template literal with an interpolation is genuinely unknowable
380
+ * statically, so it surfaces as '?' per the EndpointRecord contract rather
381
+ * than causing the endpoint to be dropped.
382
+ */
383
+ function resolveRoutePath(node, source, assignments) {
384
+ if (!node)
385
+ return { path: '?', confidence: types_1.Confidence.UNRESOLVED };
386
+ if ((0, astHelpers_1.isStringLiteral)(node)) {
387
+ return { path: (0, astHelpers_1.stringLiteralValue)(node, source), confidence: types_1.Confidence.STATIC_LITERAL };
388
+ }
389
+ if (node.type === 'template_string' || node.type === 'template_literal') {
390
+ const interpolated = node.namedChildren.some(c => c.type === 'template_substitution' || c.type === 'template_expr');
391
+ if (interpolated)
392
+ return { path: '?', confidence: types_1.Confidence.UNRESOLVED };
393
+ const raw = source.slice(node.startIndex, node.endIndex);
394
+ return { path: raw.replace(/^`|`$/g, ''), confidence: types_1.Confidence.STATIC_LITERAL };
395
+ }
396
+ if ((0, astHelpers_1.isIdentifier)(node)) {
397
+ const name = source.slice(node.startIndex, node.endIndex);
398
+ const resolved = (0, astHelpers_1.resolveIdentifier)(name, assignments);
399
+ return resolved === null
400
+ ? { path: '?', confidence: types_1.Confidence.UNRESOLVED }
401
+ : { path: resolved, confidence: types_1.Confidence.INTRA_FILE_DATAFLOW };
402
+ }
403
+ return { path: '?', confidence: types_1.Confidence.UNRESOLVED };
404
+ }
405
+ /**
406
+ * Route path for a Next.js app-router file, derived from where the file sits.
407
+ *
408
+ * `app/api/products/[id]/route.ts` -> `/api/products/[id]`. Route groups
409
+ * (`(marketing)`) and parallel-route slots (`@modal`) are organisational and
410
+ * contribute nothing to the URL. Returns null when the file is not a route.
411
+ */
412
+ function nextAppRouterPath(file) {
413
+ const normalised = file.replace(/\\/g, '/');
414
+ const appMatch = /(?:^|\/)app\/(.*\/)?route\.(?:t|j)sx?$/.exec(normalised);
415
+ if (appMatch) {
416
+ const segments = (appMatch[1] ?? '')
417
+ .split('/')
418
+ .filter(s => s && !/^\(.*\)$/.test(s) && !s.startsWith('@'));
419
+ return '/' + segments.join('/');
420
+ }
421
+ // Pages router: pages/api/orders/[id].ts -> /api/orders/[id]
422
+ const pagesMatch = /(?:^|\/)pages\/(api\/.*)\.(?:t|j)sx?$/.exec(normalised);
423
+ if (pagesMatch) {
424
+ const segments = pagesMatch[1].split('/').filter(Boolean);
425
+ const last = segments[segments.length - 1];
426
+ if (last === 'index')
427
+ segments.pop();
428
+ return '/' + segments.join('/');
429
+ }
430
+ return null;
431
+ }
432
+ /** HTTP methods Next.js app-router files export as named functions. */
433
+ const NEXT_METHOD_EXPORTS = new Set([
434
+ 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS',
435
+ ]);
436
+ /** Path params declared by a route path, across the frameworks' syntaxes. */
437
+ function pathParams(routePath) {
438
+ const out = [];
439
+ const push = (name, validated) => {
440
+ if (!name || out.some(p => p.name === name))
441
+ return;
442
+ out.push({
443
+ name,
444
+ type: 'unknown',
445
+ source: 'path',
446
+ validated,
447
+ confidence: types_1.Confidence.STATIC_LITERAL,
448
+ });
449
+ };
450
+ // Next.js: [id], [...slug], [[...slug]]
451
+ for (const m of routePath.matchAll(/\[+\.{0,3}([^\]]+?)\]+/g))
452
+ push(m[1], false);
453
+ // Django: <int:pk>, <slug>. A converter coerces and rejects, which is
454
+ // validation performed by the framework rather than a validator library.
455
+ for (const m of routePath.matchAll(/<(?:(\w+):)?(\w+)>/g))
456
+ push(m[2], Boolean(m[1]));
457
+ return out;
458
+ }
459
+ // ---------------------------------------------------------------------------
460
+ // Cross-file resolution
461
+ // ---------------------------------------------------------------------------
462
+ /** True for a middleware expression we cannot resolve to anything at all. */
463
+ function isDynamicExpression(text) {
464
+ return /require\s*\(|process\.env|import\s*\(|os\.environ|getenv/.test(text);
465
+ }
466
+ /** Strip a trailing call so `cors()` and `express.json()` name something. */
467
+ function calleeBase(text) {
468
+ const trimmed = text.trim();
469
+ const identifier = /^([A-Za-z_$][\w$]*)/.exec(trimmed);
470
+ return identifier ? identifier[1] : trimmed;
471
+ }
472
+ /** Auth scheme implemented by a named middleware or dependency function. */
473
+ function authOfName(name, file, root, source, imports, ctx) {
474
+ const base = calleeBase(name);
475
+ const spec = imports.get(base);
476
+ const dep = spec && ctx ? ctx.deps.get(spec) : undefined;
477
+ if (dep) {
478
+ const fn = findFunctionByName(dep.root, dep.source, base);
479
+ if (fn)
480
+ return (0, detectors_1.detectAuthInFunction)(fn, dep.source);
481
+ return (0, detectors_1.detectAuth)(dep.imports, dep.root, dep.source);
482
+ }
483
+ const local = findFunctionByName(root, source, base);
484
+ if (local)
485
+ return (0, detectors_1.detectAuthInFunction)(local, source);
486
+ void file;
487
+ return 'none';
488
+ }
489
+ /** True iff a Next.js middleware matcher covers this route path. */
490
+ function matchesNextMatcher(routePath, matchers) {
491
+ for (const raw of matchers) {
492
+ const prefix = raw.replace(/\/?:[\w]+\*?$/, '').replace(/\/?\(\.\*\)$/, '');
493
+ if (prefix === raw) {
494
+ if (routePath === raw)
495
+ return true;
496
+ continue;
497
+ }
498
+ if (routePath === prefix || routePath.startsWith(prefix + '/'))
499
+ return true;
500
+ }
501
+ return false;
502
+ }
503
+ /** FastAPI `Depends(name)` references inside a node. */
504
+ function dependsNames(node, source) {
505
+ const out = [];
506
+ for (const n of (0, astHelpers_1.walk)(node)) {
507
+ if (n.type !== 'call')
508
+ continue;
509
+ const p = (0, astHelpers_1.callParts)(n, source);
510
+ if (!p || p.receiver !== null || p.method !== 'Depends')
511
+ continue;
512
+ const arg = p.args[0];
513
+ if (!arg)
514
+ continue;
515
+ out.push(source.slice(arg.startIndex, arg.endIndex).trim());
516
+ }
517
+ return out;
518
+ }
519
+ /** Data layer this file uses, following one hop through a local re-export. */
520
+ function resolveDataLayer(imports, root, source, ctx) {
521
+ const direct = (0, detectors_1.detectDataLayer)(imports);
522
+ if (direct !== 'none') {
523
+ return { layer: direct, bindings: (0, detectors_1.dataLayerBindings)(direct, imports, root, source) };
524
+ }
525
+ // A local module can be the data layer: `lib/prisma.ts` constructs the
526
+ // client and re-exports it, so the route file never imports @prisma/client.
527
+ if (!ctx)
528
+ return { layer: 'none', bindings: new Set() };
529
+ for (const [spec, dep] of ctx.deps) {
530
+ const depLayer = (0, detectors_1.detectDataLayer)(dep.imports);
531
+ if (depLayer === 'none')
532
+ continue;
533
+ const seed = [...imports.entries()]
534
+ .filter(([, s]) => s === spec)
535
+ .map(([local]) => local);
536
+ if (seed.length === 0)
537
+ continue;
538
+ return {
539
+ layer: depLayer,
540
+ bindings: (0, detectors_1.dataLayerBindings)(depLayer, imports, root, source, seed),
541
+ };
542
+ }
543
+ return { layer: 'none', bindings: new Set() };
544
+ }
545
+ /** True iff this handler actually touches the data layer the file provides. */
546
+ function usesDataLayer(handlerText, layer, bindings) {
547
+ if (layer === 'none')
548
+ return false;
549
+ if (layer === 'django-orm' && /\.objects\./.test(handlerText))
550
+ return true;
551
+ for (const b of bindings) {
552
+ if (new RegExp(`\\b${b.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`).test(handlerText)) {
553
+ return true;
554
+ }
555
+ }
556
+ return false;
557
+ }
558
+ // ---------------------------------------------------------------------------
559
+ // Extraction
560
+ // ---------------------------------------------------------------------------
561
+ /**
562
+ * Extract all endpoints + global middleware from a parsed file.
563
+ *
564
+ * @param file workspace-relative file path
565
+ * @param source file source text
566
+ * @param root parsed Tree-sitter root node
567
+ * @param ctx cross-file facts, when the caller could resolve them
568
+ * @returns Layer1Result (endpoints, global middleware, imports)
569
+ */
570
+ function extractLayer1(file, source, root, ctx) {
571
+ const assignments = (0, astHelpers_1.collectStringAssignments)(root, source);
572
+ const imports = (0, astHelpers_1.collectImports)(root, source);
573
+ const schemas = (0, schemas_1.collectSchemas)(root, source, imports);
574
+ const fileAuth = (0, detectors_1.detectAuth)(imports, root, source);
575
+ const data = resolveDataLayer(imports, root, source, ctx);
576
+ const endpoints = [];
577
+ const globalMiddleware = [];
578
+ const drafts = [];
579
+ // Pass 1: collect app.use(...) global middleware registrations in order.
580
+ for (const n of (0, astHelpers_1.walk)(root)) {
581
+ const use = (0, detectors_1.isUseRegistration)(n, source);
582
+ if (!use)
583
+ continue;
584
+ // app.use('/api', auth) -> pathPrefix='/api', mw=auth
585
+ // app.use(cors) -> pathPrefix=null, mw=cors
586
+ let pathPrefix = null;
587
+ let mwIndex = 0;
588
+ if (use.args.length >= 2 && (0, astHelpers_1.isStringLiteral)(use.args[0])) {
589
+ pathPrefix = (0, astHelpers_1.stringLiteralValue)(use.args[0], source);
590
+ mwIndex = 1;
591
+ }
592
+ for (let i = mwIndex; i < use.args.length; i++) {
593
+ const arg = use.args[i];
594
+ const rawName = (0, astHelpers_1.isIdentifier)(arg) ? source.slice(arg.startIndex, arg.endIndex) : (0, parserLoader_1.nodeText)(arg, source);
595
+ const name = calleeBase(rawName);
596
+ // `app.use(cors())` registers the result of calling an import, so
597
+ // the import has to be looked up under the callee's name.
598
+ const importedFrom = imports.get(name) ?? imports.get(calleeBase(name));
599
+ globalMiddleware.push({
600
+ name,
601
+ registrationLine: use.line + 1,
602
+ sourceFile: importedFrom ?? file,
603
+ pathPrefix,
604
+ confidence: importedFrom ? types_1.Confidence.STATIC_LITERAL : types_1.Confidence.INTRA_FILE_DATAFLOW,
605
+ });
606
+ }
607
+ }
608
+ // Router-level FastAPI dependencies: APIRouter(dependencies=[Depends(x)])
609
+ // applies to every route registered on that router.
610
+ const routerDependencies = new Map();
611
+ for (const n of (0, astHelpers_1.walk)(root)) {
612
+ if (n.type !== 'variable_declarator' && n.type !== 'assignment')
613
+ continue;
614
+ const target = n.child(0);
615
+ const value = n.child(n.childCount - 1);
616
+ if (!target || !value || target === value || !(0, astHelpers_1.isIdentifier)(target))
617
+ continue;
618
+ if (value.type !== 'call' && value.type !== 'call_expression')
619
+ continue;
620
+ const p = (0, astHelpers_1.callParts)(value, source);
621
+ if (!p || (p.method !== 'APIRouter' && p.method !== 'FastAPI'))
622
+ continue;
623
+ routerDependencies.set(source.slice(target.startIndex, target.endIndex), dependsNames(value, source));
624
+ }
625
+ // Pass 2: router-object route registrations (Express/Fastify/Koa/FastAPI).
626
+ for (const n of (0, astHelpers_1.walk)(root)) {
627
+ const route = (0, detectors_1.isRouteRegistration)(n, source);
628
+ if (!route)
629
+ continue;
630
+ const resolved = resolveRoutePath(route.args[0], source, assignments);
631
+ // A Python decorator route (`@app.get("/x")`) carries only route
632
+ // configuration in its arguments; the handler is the function the
633
+ // decorator is attached to. Reading the last argument as the handler
634
+ // there picks up the path string and binds nothing.
635
+ const decoratedFn = decoratedFunction(n);
636
+ const handlerNode = route.args[route.args.length - 1];
637
+ const routeMiddlewareNodes = decoratedFn ? [] : route.args.slice(1, route.args.length - 1);
638
+ let handlerFn = decoratedFn;
639
+ if (!handlerFn && handlerNode && (0, astHelpers_1.isIdentifier)(handlerNode)) {
640
+ handlerFn = findFunctionByName(root, source, source.slice(handlerNode.startIndex, handlerNode.endIndex));
641
+ }
642
+ else if (!handlerFn && handlerNode && (handlerNode.type === 'arrow_function' || handlerNode.type === 'function_expression')) {
643
+ handlerFn = handlerNode;
644
+ }
645
+ const handlerName = decoratedFn
646
+ ? handlerNameOf(decoratedFn, source)
647
+ : (handlerNode && (0, astHelpers_1.isIdentifier)(handlerNode)
648
+ ? source.slice(handlerNode.startIndex, handlerNode.endIndex)
649
+ : handlerNameOf(handlerFn, source));
650
+ const middleware = [];
651
+ for (const g of globalMiddleware) {
652
+ if (g.pathPrefix === null || resolved.path === '?' || resolved.path.startsWith(g.pathPrefix)) {
653
+ middleware.push({
654
+ name: g.name,
655
+ sourceFile: g.sourceFile,
656
+ registrationLine: g.registrationLine,
657
+ confidence: g.confidence,
658
+ });
659
+ }
660
+ }
661
+ for (const mwn of routeMiddlewareNodes) {
662
+ const name = (0, astHelpers_1.isIdentifier)(mwn) ? source.slice(mwn.startIndex, mwn.endIndex) : (0, parserLoader_1.nodeText)(mwn, source);
663
+ const importedFrom = imports.get(name) ?? imports.get(calleeBase(name));
664
+ middleware.push({
665
+ name,
666
+ sourceFile: importedFrom ?? file,
667
+ registrationLine: route.line + 1,
668
+ confidence: importedFrom ? types_1.Confidence.STATIC_LITERAL : types_1.Confidence.INTRA_FILE_DATAFLOW,
669
+ });
670
+ }
671
+ const dependencies = [
672
+ ...(route.receiver ? routerDependencies.get(route.receiver) ?? [] : []),
673
+ ...(decoratedFn ? dependsNames(decoratedFn, source) : []),
674
+ ];
675
+ drafts.push({
676
+ framework: decoratedFn ? 'fastapi' : 'express',
677
+ method: (0, detectors_1.normalizeMethod)(route.method),
678
+ path: resolved.path,
679
+ pathConfidence: resolved.confidence,
680
+ line: route.line + 1,
681
+ handlerName,
682
+ handler: handlerFn ? { node: handlerFn, source, file, imports, schemas } : null,
683
+ middleware,
684
+ inherited: ctx?.inherited ?? [],
685
+ seedParams: [],
686
+ dependencies,
687
+ decorators: decoratedFn ? decoratorsOf(decoratedFn, source) : [],
688
+ });
689
+ }
690
+ // Pass 3: Next.js app router — method and path come from the filesystem,
691
+ // so there is no registration call to find.
692
+ const nextPath = nextAppRouterPath(file);
693
+ if (nextPath !== null) {
694
+ for (const n of (0, astHelpers_1.walk)(root)) {
695
+ if (n.type !== 'function_declaration' && n.type !== 'generator_function_declaration')
696
+ continue;
697
+ const id = n.namedChildren.find(c => (0, astHelpers_1.isIdentifier)(c));
698
+ if (!id)
699
+ continue;
700
+ const name = source.slice(id.startIndex, id.endIndex);
701
+ if (!NEXT_METHOD_EXPORTS.has(name))
702
+ continue;
703
+ if (!(0, astHelpers_1.firstAncestor)(n, a => a.type === 'export_statement'))
704
+ continue;
705
+ drafts.push({
706
+ framework: 'nextjs',
707
+ method: name,
708
+ path: nextPath,
709
+ pathConfidence: types_1.Confidence.STATIC_LITERAL,
710
+ line: n.startPosition.row + 1,
711
+ handlerName: name,
712
+ handler: { node: n, source, file, imports, schemas },
713
+ middleware: [],
714
+ inherited: ctx?.inherited ?? [],
715
+ seedParams: pathParams(nextPath),
716
+ dependencies: [],
717
+ decorators: [],
718
+ });
719
+ }
720
+ }
721
+ // Pass 4: Django URLconf. The registration site says nothing about the
722
+ // method, the params or the response — all of that lives in the view.
723
+ for (const n of (0, astHelpers_1.walk)(root)) {
724
+ const entry = (0, detectors_1.isDjangoRouteRegistration)(n, source);
725
+ if (!entry)
726
+ continue;
727
+ const routePath = (0, astHelpers_1.stringLiteralValue)(entry.path, source);
728
+ const view = entry.view ? resolveDjangoView(entry.view, source, root, imports, ctx) : null;
729
+ drafts.push({
730
+ framework: 'django',
731
+ method: view ? djangoMethod(view.decorators) : 'GET',
732
+ path: routePath,
733
+ pathConfidence: types_1.Confidence.STATIC_LITERAL,
734
+ line: entry.line + 1,
735
+ handlerName: view?.name ?? '<anonymous>',
736
+ handler: view?.handler ?? null,
737
+ middleware: [],
738
+ inherited: ctx?.inherited ?? [],
739
+ seedParams: pathParams(routePath),
740
+ dependencies: [],
741
+ decorators: view?.decorators ?? [],
742
+ });
743
+ }
744
+ // Shared per-endpoint pass.
745
+ for (const d of drafts) {
746
+ endpoints.push(buildEndpoint(d, file, root, source, imports, fileAuth, data, ctx));
747
+ }
748
+ return {
749
+ endpoints,
750
+ globalMiddleware,
751
+ imports: Object.fromEntries(imports),
752
+ };
753
+ }
754
+ /** Resolve `views.article_list` in a URLconf to the view function's body. */
755
+ function resolveDjangoView(node, source, root, imports, ctx) {
756
+ const text = source.slice(node.startIndex, node.endIndex).trim();
757
+ const segments = text.split('.');
758
+ const name = segments[segments.length - 1];
759
+ const moduleAlias = segments.length > 1 ? segments[0] : name;
760
+ const spec = imports.get(moduleAlias) ?? imports.get(name);
761
+ const dep = spec && ctx ? ctx.deps.get(spec) : undefined;
762
+ if (dep) {
763
+ const fn = findFunctionByName(dep.root, dep.source, name);
764
+ if (fn) {
765
+ return {
766
+ name,
767
+ handler: {
768
+ node: fn,
769
+ source: dep.source,
770
+ file: dep.file,
771
+ imports: dep.imports,
772
+ schemas: (0, schemas_1.collectSchemas)(dep.root, dep.source, dep.imports),
773
+ },
774
+ decorators: decoratorsOf(fn, dep.source),
775
+ };
776
+ }
777
+ }
778
+ const local = findFunctionByName(root, source, name);
779
+ if (local) {
780
+ return {
781
+ name,
782
+ handler: { node: local, source, file: '', imports, schemas: new Map() },
783
+ decorators: decoratorsOf(local, source),
784
+ };
785
+ }
786
+ return { name, handler: null, decorators: [] };
787
+ }
788
+ /** HTTP method a Django view is restricted to by its decorators. */
789
+ function djangoMethod(decorators) {
790
+ for (const d of decorators) {
791
+ if (/@require_POST\b/.test(d))
792
+ return 'POST';
793
+ if (/@require_GET\b/.test(d))
794
+ return 'GET';
795
+ if (/@require_safe\b/.test(d))
796
+ return 'GET';
797
+ const m = /@require_http_methods\(\s*\[([^\]]*)\]/.exec(d);
798
+ if (m) {
799
+ const methods = [...m[1].matchAll(/['"](\w+)['"]/g)].map(x => x[1].toUpperCase());
800
+ if (methods.length === 1)
801
+ return methods[0];
802
+ if (methods.length > 1)
803
+ return 'ALL';
804
+ }
805
+ }
806
+ return 'GET';
807
+ }
808
+ /** Auth a Django view's decorators establish. */
809
+ function djangoDecoratorAuth(decorators) {
810
+ for (const d of decorators) {
811
+ if (/@login_required|@permission_required|@staff_member_required|@user_passes_test/.test(d)) {
812
+ return 'session';
813
+ }
814
+ }
815
+ return 'none';
816
+ }
817
+ /** Turn a draft into the finished record: params, calls, auth, ORM, confidence. */
818
+ function buildEndpoint(d, file, root, source, imports, fileAuth, data, ctx) {
819
+ const middleware = [
820
+ ...d.inherited.map(m => ({
821
+ name: m.name,
822
+ sourceFile: m.sourceFile,
823
+ registrationLine: m.registrationLine,
824
+ confidence: m.confidence,
825
+ })),
826
+ ...d.middleware,
827
+ ];
828
+ let params = d.seedParams;
829
+ let callGraph = [];
830
+ let responseShape = 'unknown';
831
+ let dataLayer = 'none';
832
+ if (d.handler) {
833
+ params = extractParams(d.handler, d.seedParams, d.framework);
834
+ callGraph = extractCallGraph(d.handler.node, d.handler.source, d.handler.imports);
835
+ responseShape = (0, detectors_1.detectResponseShape)(d.handler.node, d.handler.source);
836
+ const handlerText = d.handler.source.slice(d.handler.node.startIndex, d.handler.node.endIndex);
837
+ // Django's route file and view file are different files, so the data
838
+ // layer has to be judged from whichever file the body lives in.
839
+ const scope = d.handler.file && d.handler.file !== file
840
+ ? resolveDataLayer(d.handler.imports, d.handler.node, d.handler.source, undefined)
841
+ : data;
842
+ if (usesDataLayer(handlerText, scope.layer, scope.bindings)) {
843
+ dataLayer = (0, detectors_1.toDataLayer)(scope.layer);
844
+ }
845
+ }
846
+ // FastAPI serialises whatever the handler returns, so a handler that never
847
+ // touches a response object still answers with JSON.
848
+ if (responseShape === 'unknown' && d.framework === 'fastapi')
849
+ responseShape = 'json';
850
+ // The validator that covers THIS endpoint is the one its params went
851
+ // through — a sibling route's zod import says nothing about this route.
852
+ const validated = params.find(p => p.validator);
853
+ const validatorLibrary = validated?.validator ?? 'none';
854
+ const authScheme = resolveAuth(d, middleware, file, root, source, imports, fileAuth, ctx);
855
+ const allConfs = [
856
+ d.pathConfidence,
857
+ ...middleware.map(m => m.confidence),
858
+ ...params.map(p => p.confidence),
859
+ ...callGraph.map(c => c.confidence),
860
+ ];
861
+ const mounted = ctx?.mountPrefix ? (0, crossFile_1.joinMountedPath)(ctx.mountPrefix, d.path) : undefined;
862
+ return {
863
+ id: `${file}:${d.line}:${d.method}:${d.path}`,
864
+ method: d.method,
865
+ path: d.path,
866
+ mountedPath: mounted && mounted !== d.path ? mounted : undefined,
867
+ handlerName: d.handlerName,
868
+ sourceFile: file,
869
+ line: d.line,
870
+ middleware,
871
+ params,
872
+ authScheme,
873
+ dataLayer,
874
+ validatorLibrary,
875
+ callGraph,
876
+ responseShape,
877
+ pathConfidence: d.pathConfidence,
878
+ confidence: Math.min(...allConfs),
879
+ runtimeConfirmed: false,
880
+ };
881
+ }
882
+ /**
883
+ * Which auth scheme actually guards this endpoint.
884
+ *
885
+ * Answered from the guards themselves — the middleware chain (including the
886
+ * chain inherited from wherever the router is mounted), framework
887
+ * dependencies, and decorators — rather than from what the file imports. A
888
+ * router file behind `app.use('/api', requireAuth)` imports nothing
889
+ * auth-related and is still authenticated; a file that imports `jsonwebtoken`
890
+ * for one route does not thereby protect the others.
891
+ */
892
+ function resolveAuth(d, middleware, file, root, source, imports, fileAuth, ctx) {
893
+ // Next.js edge middleware guards by URL pattern, not by import.
894
+ if (ctx?.nextMiddleware && matchesNextMatcher(d.path, ctx.nextMiddleware.matchers)) {
895
+ return ctx.nextMiddleware.auth;
896
+ }
897
+ const fromDecorator = djangoDecoratorAuth(d.decorators);
898
+ if (fromDecorator !== 'none')
899
+ return fromDecorator;
900
+ for (const dep of d.dependencies) {
901
+ const scheme = authOfName(dep, file, root, source, imports, ctx);
902
+ if (scheme !== 'none')
903
+ return scheme;
904
+ }
905
+ let sawDynamic = false;
906
+ for (const m of d.inherited) {
907
+ if (m.dynamic) {
908
+ sawDynamic = true;
909
+ continue;
910
+ }
911
+ if (m.auth !== 'none')
912
+ return m.auth;
913
+ }
914
+ for (const m of middleware) {
915
+ if (isDynamicExpression(m.name)) {
916
+ sawDynamic = true;
917
+ continue;
918
+ }
919
+ const scheme = authOfName(m.name, file, root, source, imports, ctx);
920
+ if (scheme !== 'none')
921
+ return scheme;
922
+ }
923
+ // A guard we could not resolve at all is not the same as no guard.
924
+ if (sawDynamic)
925
+ return 'unknown';
926
+ return fileAuth;
927
+ }
928
+ //# sourceMappingURL=layer1.js.map