eslint-plugin-vercel-ai-security 1.4.0 → 1.4.2

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 (48) hide show
  1. package/package.json +5 -20
  2. package/src/index.d.ts +17 -74
  3. package/src/index.js +1 -99
  4. package/src/oxlint.d.ts +17 -20
  5. package/src/oxlint.js +0 -18
  6. package/src/rules/no-dynamic-system-prompt/index.js +1 -23
  7. package/src/rules/no-hardcoded-api-keys/index.js +5 -28
  8. package/src/rules/no-sensitive-in-prompt/index.js +1 -21
  9. package/src/rules/no-system-prompt-leak/index.js +0 -25
  10. package/src/rules/no-training-data-exposure/index.js +0 -16
  11. package/src/rules/no-unsafe-output-handling/index.js +0 -35
  12. package/src/rules/require-abort-signal/index.js +0 -14
  13. package/src/rules/require-audit-logging/index.js +0 -24
  14. package/src/rules/require-embedding-validation/index.js +0 -23
  15. package/src/rules/require-error-handling/index.js +0 -19
  16. package/src/rules/require-max-steps/index.js +0 -18
  17. package/src/rules/require-max-tokens/index.js +0 -14
  18. package/src/rules/require-output-filtering/index.js +0 -23
  19. package/src/rules/require-output-validation/index.js +0 -23
  20. package/src/rules/require-rag-content-validation/index.js +1 -29
  21. package/src/rules/require-request-timeout/index.js +0 -16
  22. package/src/rules/require-tool-confirmation/index.js +1 -25
  23. package/src/rules/require-tool-schema/index.js +0 -19
  24. package/src/rules/require-validated-prompt/index.js +1 -34
  25. package/src/types/index.js +0 -5
  26. package/src/utils/prompt-props.js +8 -28
  27. package/AGENTS.md +0 -131
  28. package/CHANGELOG.md +0 -255
  29. package/src/rules/no-dynamic-system-prompt/index.d.ts +0 -14
  30. package/src/rules/no-hardcoded-api-keys/index.d.ts +0 -14
  31. package/src/rules/no-sensitive-in-prompt/index.d.ts +0 -14
  32. package/src/rules/no-system-prompt-leak/index.d.ts +0 -14
  33. package/src/rules/no-training-data-exposure/index.d.ts +0 -14
  34. package/src/rules/no-unsafe-output-handling/index.d.ts +0 -15
  35. package/src/rules/require-abort-signal/index.d.ts +0 -14
  36. package/src/rules/require-audit-logging/index.d.ts +0 -14
  37. package/src/rules/require-embedding-validation/index.d.ts +0 -16
  38. package/src/rules/require-error-handling/index.d.ts +0 -14
  39. package/src/rules/require-max-steps/index.d.ts +0 -14
  40. package/src/rules/require-max-tokens/index.d.ts +0 -16
  41. package/src/rules/require-output-filtering/index.d.ts +0 -16
  42. package/src/rules/require-output-validation/index.d.ts +0 -16
  43. package/src/rules/require-rag-content-validation/index.d.ts +0 -16
  44. package/src/rules/require-request-timeout/index.d.ts +0 -14
  45. package/src/rules/require-tool-confirmation/index.d.ts +0 -14
  46. package/src/rules/require-tool-schema/index.d.ts +0 -15
  47. package/src/rules/require-validated-prompt/index.d.ts +0 -19
  48. package/src/utils/prompt-props.d.ts +0 -27
@@ -1,16 +1,6 @@
1
1
  "use strict";
2
- /**
3
- * Copyright (c) 2025 Ofri Peretz
4
- * Licensed under the MIT License. Use of this source code is governed by the
5
- * MIT license that can be found in the LICENSE file.
6
- */
7
2
  Object.defineProperty(exports, "__esModule", { value: true });
8
3
  exports.requireRagContentValidation = void 0;
9
- /**
10
- * @fileoverview Require validation of RAG content before use in prompts
11
- * @description Detects when retrieved documents are used directly without sanitization
12
- * @see OWASP ASI07: Poisoned RAG Pipeline
13
- */
14
4
  const eslint_devkit_1 = require("@interlace/eslint-devkit");
15
5
  const prompt_props_1 = require("../../utils/prompt-props");
16
6
  exports.requireRagContentValidation = (0, eslint_devkit_1.createRule)({
@@ -78,13 +68,8 @@ exports.requireRagContentValidation = (0, eslint_devkit_1.createRule)({
78
68
  'validate', 'sanitize', 'filter', 'clean',
79
69
  ];
80
70
  const sourceCode = context.sourceCode;
81
- // Vercel AI SDK functions
82
71
  const aiSDKFunctions = ['generateText', 'streamText', 'generateObject', 'streamObject'];
83
- // Track variables that hold RAG content
84
72
  const ragVariables = new Set();
85
- /**
86
- * Check if expression is a RAG retrieval call
87
- */
88
73
  function isRagCall(node) {
89
74
  if (node.type !== 'CallExpression')
90
75
  return null;
@@ -96,16 +81,10 @@ exports.requireRagContentValidation = (0, eslint_devkit_1.createRule)({
96
81
  }
97
82
  return null;
98
83
  }
99
- /**
100
- * Check if expression is wrapped in validation
101
- */
102
84
  function isValidated(node) {
103
85
  const callee = sourceCode.getText(node.callee);
104
86
  return validatorFunctions.some((fn) => callee.toLowerCase().includes(fn.toLowerCase()));
105
87
  }
106
- /**
107
- * Check if an expression contains RAG content
108
- */
109
88
  function containsRagContent(node) {
110
89
  if (node.type === 'Identifier' && ragVariables.has(node.name)) {
111
90
  return node.name;
@@ -117,12 +96,10 @@ exports.requireRagContentValidation = (0, eslint_devkit_1.createRule)({
117
96
  return rag;
118
97
  }
119
98
  }
120
- // Handle: ${await retrieve(query)}
121
99
  if (node.type === 'AwaitExpression') {
122
100
  return containsRagContent(node.argument);
123
101
  }
124
102
  if (node.type === 'CallExpression') {
125
- // Check if this is an unvalidated RAG call
126
103
  const ragSource = isRagCall(node);
127
104
  if (ragSource && !isValidated(node)) {
128
105
  return ragSource;
@@ -131,13 +108,11 @@ exports.requireRagContentValidation = (0, eslint_devkit_1.createRule)({
131
108
  return null;
132
109
  }
133
110
  return {
134
- // Track RAG variable assignments
135
111
  VariableDeclarator(node) {
136
112
  if (node.id.type !== 'Identifier')
137
113
  return;
138
114
  if (!node.init)
139
115
  return;
140
- // Handle: const docs = await vectorStore.search(query);
141
116
  let initNode = node.init;
142
117
  if (initNode.type === 'AwaitExpression') {
143
118
  initNode = initNode.argument;
@@ -148,19 +123,16 @@ exports.requireRagContentValidation = (0, eslint_devkit_1.createRule)({
148
123
  },
149
124
  CallExpression(node) {
150
125
  const callee = sourceCode.getText(node.callee);
151
- // Check if this is an AI SDK function
152
126
  const isAIFunction = aiSDKFunctions.some(fn => callee.includes(fn));
153
127
  if (!isAIFunction)
154
128
  return;
155
- // Check first argument (options object)
156
129
  const optionsArg = node.arguments[0];
157
130
  if (!optionsArg || optionsArg.type !== 'ObjectExpression')
158
131
  return;
159
- // Check prompt property
160
132
  for (const prop of optionsArg.properties) {
161
133
  if (prop.type !== eslint_devkit_1.AST_NODE_TYPES.Property)
162
134
  continue;
163
- const keyName = (0, prompt_props_1.getStaticPropName)(prop.key);
135
+ const keyName = (0, prompt_props_1.getStaticPropName)(prop);
164
136
  if (keyName !== 'prompt' && !(0, prompt_props_1.isSystemPromptProp)(keyName))
165
137
  continue;
166
138
  const ragSource = containsRagContent(prop.value);
@@ -1,16 +1,6 @@
1
1
  "use strict";
2
- /**
3
- * Copyright (c) 2025 Ofri Peretz
4
- * Licensed under the MIT License. Use of this source code is governed by the
5
- * MIT license that can be found in the LICENSE file.
6
- */
7
2
  Object.defineProperty(exports, "__esModule", { value: true });
8
3
  exports.requireRequestTimeout = void 0;
9
- /**
10
- * @fileoverview Require timeout for AI API calls to prevent DoS
11
- * @description Detects AI calls without timeout configuration
12
- * @see OWASP LLM04: Model Denial of Service
13
- */
14
4
  const eslint_devkit_1 = require("@interlace/eslint-devkit");
15
5
  exports.requireRequestTimeout = (0, eslint_devkit_1.createRule)({
16
6
  name: 'require-request-timeout',
@@ -59,22 +49,17 @@ exports.requireRequestTimeout = (0, eslint_devkit_1.createRule)({
59
49
  const allowInTests = options.allowInTests ?? true;
60
50
  const sourceCode = context.sourceCode;
61
51
  const filename = context.filename;
62
- // Skip test files if allowed
63
52
  if (allowInTests && /\.(test|spec)\.[jt]sx?$/.test(filename)) {
64
53
  return {};
65
54
  }
66
- // Vercel AI SDK functions
67
55
  const aiSDKFunctions = ['generateText', 'streamText', 'generateObject', 'streamObject'];
68
- // Timeout-related property names
69
56
  const timeoutProperties = new Set(['timeout', 'abortSignal', 'signal', 'timeoutMs', 'requestTimeout']);
70
57
  return {
71
58
  CallExpression(node) {
72
59
  const callee = sourceCode.getText(node.callee);
73
- // Check if this is an AI SDK function
74
60
  const matchedFunction = aiSDKFunctions.find(fn => callee.includes(fn));
75
61
  if (!matchedFunction)
76
62
  return;
77
- // Check first argument (options object)
78
63
  const optionsArg = node.arguments[0];
79
64
  if (!optionsArg || optionsArg.type !== 'ObjectExpression') {
80
65
  context.report({
@@ -84,7 +69,6 @@ exports.requireRequestTimeout = (0, eslint_devkit_1.createRule)({
84
69
  });
85
70
  return;
86
71
  }
87
- // Check for timeout or abort signal properties
88
72
  const hasTimeout = optionsArg.properties.some(prop => {
89
73
  if (prop.type !== 'Property')
90
74
  return false;
@@ -1,17 +1,6 @@
1
1
  "use strict";
2
- /**
3
- * Copyright (c) 2025 Ofri Peretz
4
- * Licensed under the MIT License. Use of this source code is governed by the
5
- * MIT license that can be found in the LICENSE file.
6
- */
7
2
  Object.defineProperty(exports, "__esModule", { value: true });
8
3
  exports.requireToolConfirmation = void 0;
9
- /**
10
- * @fileoverview Require human confirmation for destructive tool operations
11
- * @description Ensures tools that perform destructive actions have confirmation
12
- * @see OWASP ASI09: Human-Agent Trust Exploitation
13
- * @see OWASP LLM06: Excessive Agency
14
- */
15
4
  const eslint_devkit_1 = require("@interlace/eslint-devkit");
16
5
  exports.requireToolConfirmation = (0, eslint_devkit_1.createRule)({
17
6
  name: 'require-tool-confirmation',
@@ -67,9 +56,6 @@ exports.requireToolConfirmation = (0, eslint_devkit_1.createRule)({
67
56
  const destructivePatterns = options.destructivePatterns ?? [
68
57
  'delete', 'remove', 'transfer', 'execute', 'update', 'create',
69
58
  ];
70
- /**
71
- * Check if a tool name suggests destructive operation
72
- */
73
59
  function isDestructiveTool(name) {
74
60
  const lowerName = name.toLowerCase();
75
61
  for (const pattern of destructivePatterns) {
@@ -79,15 +65,11 @@ exports.requireToolConfirmation = (0, eslint_devkit_1.createRule)({
79
65
  }
80
66
  return null;
81
67
  }
82
- /**
83
- * Check if tool has confirmation requirement
84
- */
85
- // oxlint-disable-next-line consistent-function-scoping
86
68
  function hasConfirmationFlag(toolDef) {
87
69
  const confirmationProps = new Set([
88
70
  'requiresConfirmation', 'requireConfirmation', 'confirmation',
89
71
  'requiresApproval', 'requireApproval', 'approval',
90
- 'dangerouslyAllowBrowser', // If explicitly acknowledged
72
+ 'dangerouslyAllowBrowser',
91
73
  ]);
92
74
  return toolDef.properties.some(prop => {
93
75
  if (prop.type !== 'Property')
@@ -98,21 +80,17 @@ exports.requireToolConfirmation = (0, eslint_devkit_1.createRule)({
98
80
  }
99
81
  return {
100
82
  Property(node) {
101
- // Looking for tool definitions in tools object
102
83
  if (node.key.type !== 'Identifier' && node.key.type !== 'Literal')
103
84
  return;
104
85
  const toolName = node.key.type === 'Identifier'
105
86
  ? node.key.name
106
87
  : String(node.key.value);
107
- // Check if this looks like a tool definition
108
88
  if (node.value.type !== 'ObjectExpression' && node.value.type !== 'CallExpression') {
109
89
  return;
110
90
  }
111
- // Check if tool name suggests destructive operation
112
91
  const destructiveOp = isDestructiveTool(toolName);
113
92
  if (!destructiveOp)
114
93
  return;
115
- // Check the parent to ensure this is in a tools object
116
94
  const parent = node.parent;
117
95
  if (parent?.type !== 'ObjectExpression')
118
96
  return;
@@ -122,7 +100,6 @@ exports.requireToolConfirmation = (0, eslint_devkit_1.createRule)({
122
100
  const toolsKey = grandparent.key;
123
101
  if (toolsKey.type !== 'Identifier' || toolsKey.name !== 'tools')
124
102
  return;
125
- // For object expressions, check for confirmation flag
126
103
  if (node.value.type === 'ObjectExpression') {
127
104
  if (!hasConfirmationFlag(node.value)) {
128
105
  context.report({
@@ -135,7 +112,6 @@ exports.requireToolConfirmation = (0, eslint_devkit_1.createRule)({
135
112
  });
136
113
  }
137
114
  }
138
- // For CallExpressions (tool() helper), we'll assume it might be handled
139
115
  },
140
116
  };
141
117
  },
@@ -1,17 +1,6 @@
1
1
  "use strict";
2
- /**
3
- * Copyright (c) 2025 Ofri Peretz
4
- * Licensed under the MIT License. Use of this source code is governed by the
5
- * MIT license that can be found in the LICENSE file.
6
- */
7
2
  Object.defineProperty(exports, "__esModule", { value: true });
8
3
  exports.requireToolSchema = void 0;
9
- /**
10
- * @fileoverview Require Zod schema validation for tool parameters
11
- * @description Ensures all tools have proper inputSchema defined with Zod
12
- * @see https://sdk.vercel.ai/docs/ai-sdk-core/tools-and-tool-calling
13
- * @see OWASP ASI02: Tool Misuse & Exploitation
14
- */
15
4
  const eslint_devkit_1 = require("@interlace/eslint-devkit");
16
5
  exports.requireToolSchema = (0, eslint_devkit_1.createRule)({
17
6
  name: 'require-tool-schema',
@@ -70,20 +59,16 @@ exports.requireToolSchema = (0, eslint_devkit_1.createRule)({
70
59
  const allowInTests = options.allowInTests ?? false;
71
60
  const sourceCode = context.sourceCode;
72
61
  const filename = context.filename;
73
- // Skip test files if allowed
74
62
  if (allowInTests && /\.(test|spec)\.[jt]sx?$/.test(filename)) {
75
63
  return {};
76
64
  }
77
- // Vercel AI SDK functions that use tools
78
65
  const functionsWithTools = ['generateText', 'streamText', 'generateObject', 'streamObject'];
79
66
  return {
80
67
  CallExpression(node) {
81
68
  const callee = sourceCode.getText(node.callee);
82
- // Check if this is a target AI function or a tool() helper
83
69
  const isAIFunction = functionsWithTools.some(fn => callee.includes(fn));
84
70
  const isToolHelper = callee === 'tool' || callee.endsWith('.tool');
85
71
  if (isToolHelper) {
86
- // Check tool() helper for inputSchema
87
72
  const toolArg = node.arguments[0];
88
73
  if (toolArg && toolArg.type === 'ObjectExpression') {
89
74
  const hasInputSchema = toolArg.properties.some(prop => {
@@ -104,7 +89,6 @@ exports.requireToolSchema = (0, eslint_devkit_1.createRule)({
104
89
  }
105
90
  if (!isAIFunction)
106
91
  return;
107
- // Check for tools property in AI function call
108
92
  const optionsArg = node.arguments[0];
109
93
  if (!optionsArg || optionsArg.type !== 'ObjectExpression')
110
94
  return;
@@ -116,7 +100,6 @@ exports.requireToolSchema = (0, eslint_devkit_1.createRule)({
116
100
  });
117
101
  if (!toolsProp)
118
102
  return;
119
- // Check each tool in the tools object
120
103
  if (toolsProp.value.type === 'ObjectExpression') {
121
104
  for (const toolDef of toolsProp.value.properties) {
122
105
  if (toolDef.type !== 'Property')
@@ -126,7 +109,6 @@ exports.requireToolSchema = (0, eslint_devkit_1.createRule)({
126
109
  : toolDef.key.type === 'Literal'
127
110
  ? String(toolDef.key.value)
128
111
  : 'unknown';
129
- // Check if tool value is an object with inputSchema
130
112
  if (toolDef.value.type === 'ObjectExpression') {
131
113
  const hasInputSchema = toolDef.value.properties.some(prop => {
132
114
  if (prop.type !== 'Property')
@@ -142,7 +124,6 @@ exports.requireToolSchema = (0, eslint_devkit_1.createRule)({
142
124
  });
143
125
  }
144
126
  }
145
- // If it's a CallExpression (tool() helper), it will be checked separately
146
127
  }
147
128
  }
148
129
  },
@@ -1,17 +1,6 @@
1
1
  "use strict";
2
- /**
3
- * Copyright (c) 2025 Ofri Peretz
4
- * Licensed under the MIT License. Use of this source code is governed by the
5
- * MIT license that can be found in the LICENSE file.
6
- */
7
2
  Object.defineProperty(exports, "__esModule", { value: true });
8
3
  exports.requireValidatedPrompt = void 0;
9
- /**
10
- * @fileoverview Require validated prompts in generateText calls
11
- * @description Detects when user input is passed directly to generateText without validation
12
- * @see https://sdk.vercel.ai/docs/ai-sdk-core/generating-text
13
- * @see https://owasp.org/www-project-top-10-for-large-language-model-applications/
14
- */
15
4
  const eslint_devkit_1 = require("@interlace/eslint-devkit");
16
5
  const prompt_props_1 = require("../../utils/prompt-props");
17
6
  exports.requireValidatedPrompt = (0, eslint_devkit_1.createRule)({
@@ -109,42 +98,29 @@ exports.requireValidatedPrompt = (0, eslint_devkit_1.createRule)({
109
98
  const allowInTests = options.allowInTests ?? false;
110
99
  const sourceCode = context.sourceCode;
111
100
  const filename = context.filename;
112
- // Skip test files if allowed
113
101
  if (allowInTests && /\.(test|spec)\.[jt]sx?$/.test(filename)) {
114
102
  return {};
115
103
  }
116
- // Vercel AI SDK function names
117
104
  const vercelAIFunctions = [
118
105
  'generateText',
119
106
  'streamText',
120
107
  'generateObject',
121
108
  'streamObject',
122
109
  ];
123
- /**
124
- * Check if a node is a call to a validation function
125
- */
126
110
  function isValidatedCall(node) {
127
111
  if (node.type !== 'CallExpression')
128
112
  return false;
129
113
  const callee = sourceCode.getText(node.callee);
130
114
  return validatorFunctions.some((v) => callee.includes(v));
131
115
  }
132
- /**
133
- * Check if an identifier suggests user input
134
- */
135
116
  function isUserInput(node) {
136
117
  const text = sourceCode.getText(node);
137
118
  return userInputPatterns.some((pattern) => text.includes(pattern));
138
119
  }
139
- /**
140
- * Check if a node contains unsafe user input
141
- */
142
120
  function hasUnsafeUserInput(node) {
143
- // If it's validated, it's safe
144
121
  if (isValidatedCall(node)) {
145
122
  return { unsafe: false };
146
123
  }
147
- // Check for template literals with user input
148
124
  if (node.type === 'TemplateLiteral') {
149
125
  for (const expr of node.expressions) {
150
126
  const result = hasUnsafeUserInput(expr);
@@ -152,20 +128,17 @@ exports.requireValidatedPrompt = (0, eslint_devkit_1.createRule)({
152
128
  return result;
153
129
  }
154
130
  }
155
- // Check identifiers
156
131
  if (node.type === 'Identifier') {
157
132
  if (isUserInput(node)) {
158
133
  return { unsafe: true, input: node.name };
159
134
  }
160
135
  }
161
- // Check member expressions
162
136
  if (node.type === 'MemberExpression') {
163
137
  const text = sourceCode.getText(node);
164
138
  if (isUserInput(node)) {
165
139
  return { unsafe: true, input: text };
166
140
  }
167
141
  }
168
- // Check binary expressions (concatenation)
169
142
  if (node.type === 'BinaryExpression') {
170
143
  const leftResult = hasUnsafeUserInput(node.left);
171
144
  if (leftResult.unsafe)
@@ -176,26 +149,20 @@ exports.requireValidatedPrompt = (0, eslint_devkit_1.createRule)({
176
149
  }
177
150
  return { unsafe: false };
178
151
  }
179
- /**
180
- * Check generateText/streamText call for unsafe prompts
181
- */
182
152
  function checkAICall(node) {
183
153
  const callee = sourceCode.getText(node.callee);
184
- // Get which function is being called
185
154
  const matchedFunction = vercelAIFunctions.find(fn => callee.includes(fn));
186
155
  if (!matchedFunction) {
187
156
  return;
188
157
  }
189
- // Check first argument (options object)
190
158
  const optionsArg = node.arguments[0];
191
159
  if (!optionsArg || optionsArg.type !== 'ObjectExpression') {
192
160
  return;
193
161
  }
194
- // Check for unsafe prompt property
195
162
  for (const prop of optionsArg.properties) {
196
163
  if (prop.type !== eslint_devkit_1.AST_NODE_TYPES.Property)
197
164
  continue;
198
- const keyName = (0, prompt_props_1.getStaticPropName)(prop.key);
165
+ const keyName = (0, prompt_props_1.getStaticPropName)(prop);
199
166
  if (keyName === 'prompt') {
200
167
  const result = hasUnsafeUserInput(prop.value);
201
168
  if (result.unsafe) {
@@ -1,7 +1,2 @@
1
1
  "use strict";
2
- /**
3
- * Copyright (c) 2025 Ofri Peretz
4
- * Licensed under the MIT License. Use of this source code is governed by the
5
- * MIT license that can be found in the LICENSE file.
6
- */
7
2
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -1,40 +1,20 @@
1
1
  "use strict";
2
- /**
3
- * Copyright (c) 2025 Ofri Peretz
4
- * Licensed under the MIT License. Use of this source code is governed by the
5
- * MIT license that can be found in the LICENSE file.
6
- */
7
2
  Object.defineProperty(exports, "__esModule", { value: true });
8
3
  exports.SYSTEM_PROMPT_PROPS = void 0;
9
4
  exports.isSystemPromptProp = isSystemPromptProp;
10
5
  exports.getStaticPropName = getStaticPropName;
11
6
  const eslint_devkit_1 = require("@interlace/eslint-devkit");
12
- /**
13
- * Property names the AI SDK accepts for the system prompt.
14
- *
15
- * As of AI SDK v7 the canonical name is `instructions`; `system` still works but
16
- * is marked `@deprecated Use 'instructions' instead` in the SDK's own types. Both
17
- * must be checked — code written against current docs uses `instructions`, and
18
- * code predating v7 uses `system`.
19
- */
20
7
  exports.SYSTEM_PROMPT_PROPS = new Set(['instructions', 'system']);
21
- /** True when `name` is a property the AI SDK treats as the system prompt. */
22
8
  function isSystemPromptProp(name) {
23
9
  return name !== null && exports.SYSTEM_PROMPT_PROPS.has(name);
24
10
  }
25
- /**
26
- * Static name of an object property key, or `null` when it isn't statically known.
27
- *
28
- * `{ instructions: x }` and `{ 'instructions': x }` are the same property, but the
29
- * first parses to an `Identifier` key and the second to a string `Literal`. Reading
30
- * only `Identifier` silently skips the quoted form, so a rule stops firing on code
31
- * that is semantically identical. Computed keys (`{ [k]: x }`) stay `null` — the
32
- * name genuinely isn't known statically.
33
- */
34
- function getStaticPropName(key) {
35
- if (key.type === eslint_devkit_1.AST_NODE_TYPES.Identifier)
36
- return key.name;
37
- if (key.type === eslint_devkit_1.AST_NODE_TYPES.Literal && typeof key.value === 'string')
38
- return key.value;
11
+ function getStaticPropName(prop) {
12
+ if (prop.computed)
13
+ return null;
14
+ if (prop.key.type === eslint_devkit_1.AST_NODE_TYPES.Identifier)
15
+ return prop.key.name;
16
+ if (prop.key.type === eslint_devkit_1.AST_NODE_TYPES.Literal && typeof prop.key.value === 'string') {
17
+ return prop.key.value;
18
+ }
39
19
  return null;
40
20
  }
package/AGENTS.md DELETED
@@ -1,131 +0,0 @@
1
- # AGENTS.md
2
-
3
- > Context for AI coding agents working on eslint-plugin-vercel-ai-security
4
-
5
- ## Setup Commands
6
-
7
- ```bash
8
- # Install dependencies (from monorepo root)
9
- npm install
10
-
11
- # Build this package
12
- nx build eslint-plugin-vercel-ai-security
13
-
14
- # Run tests
15
- nx test eslint-plugin-vercel-ai-security
16
-
17
- # Run tests with coverage
18
- nx test eslint-plugin-vercel-ai-security --coverage
19
-
20
- # Lint this package
21
- nx lint eslint-plugin-vercel-ai-security
22
- ```
23
-
24
- ## Code Style
25
-
26
- - TypeScript strict mode with `@interlace/eslint-devkit` types
27
- - Use `AST_NODE_TYPES` constants, never string literals for node types
28
- - Use `formatLLMMessage()` for all rule error messages
29
- - Include CWE, CVSS, OWASP LLM Top 10 in every security message
30
- - Use `c8 ignore` comments with documented reasons for untestable code
31
- - Single-pass AST traversal patterns (O(n) complexity)
32
-
33
- ## Testing Instructions
34
-
35
- - Tests use `@typescript-eslint/rule-tester` with Vitest
36
- - Each rule has `index.ts` (implementation) and `*.test.ts` (tests) in same directory
37
- - Run specific rule test: `nx test eslint-plugin-vercel-ai-security --testPathPattern="require-validated-prompt"`
38
- - Coverage target: ≥90% lines, ≥95% functions
39
- - All tests must pass before committing
40
-
41
- ## Project Structure
42
-
43
- ```
44
- src/
45
- ├── index.ts # Plugin entry, exports rules and configs
46
- └── rules/ # Rule directories
47
- └── [rule-name]/
48
- ├── index.ts # Rule implementation
49
- └── *.test.ts # Rule tests
50
- ```
51
-
52
- ## Plugin Purpose
53
-
54
- Security rules for **Vercel AI SDK** (`ai` package). Covers prompt injection, sensitive data exposure, API key security, output handling, tool security, and resource limits.
55
-
56
- ## Target SDK Functions
57
-
58
- ```typescript
59
- import {
60
- generateText,
61
- streamText,
62
- generateObject,
63
- streamObject,
64
- tool,
65
- } from 'ai';
66
- import { createOpenAI, createAnthropic, createGoogle } from '@ai-sdk/*';
67
- ```
68
-
69
- ## Quick Reference: All 10 Rules
70
-
71
- | Rule | Severity | Fix Pattern |
72
- | --------------------------- | -------- | ------------------------------------ |
73
- | `require-validated-prompt` | CRITICAL | `prompt: validateInput(userInput)` |
74
- | `no-sensitive-in-prompt` | CRITICAL | Remove password/token from prompt |
75
- | `no-hardcoded-api-keys` | CRITICAL | `apiKey: process.env.OPENAI_API_KEY` |
76
- | `no-unsafe-output-handling` | CRITICAL | Don't pass `result.text` to `eval()` |
77
- | `require-tool-schema` | HIGH | Add `inputSchema: z.object({...})` |
78
- | `require-max-tokens` | HIGH | Add `maxTokens: 4096` |
79
- | `require-max-steps` | HIGH | Add `maxSteps: 5` when using tools |
80
- | `require-tool-confirmation` | HIGH | Add `requiresConfirmation: true` |
81
- | `require-error-handling` | MEDIUM | Wrap in `try { } catch { }` |
82
- | `require-abort-signal` | LOW | Add `abortSignal: controller.signal` |
83
-
84
- ## Common Fix Patterns
85
-
86
- ```typescript
87
- // Prompt Injection (CWE-74)
88
- // BAD: await generateText({ prompt: userInput })
89
- // GOOD: await generateText({ prompt: validateInput(userInput) })
90
-
91
- // API Keys (CWE-798)
92
- // BAD: createOpenAI({ apiKey: 'sk-proj-abc123...' })
93
- // GOOD: createOpenAI({ apiKey: process.env.OPENAI_API_KEY })
94
-
95
- // Tool Schema (CWE-20)
96
- // BAD: tool({ execute: async (params) => search(params) })
97
- // GOOD: tool({ inputSchema: z.object({ query: z.string().max(500) }), execute: ... })
98
-
99
- // Max Tokens (CWE-770)
100
- // BAD: await generateText({ prompt: 'Write a story' })
101
- // GOOD: await generateText({ prompt: 'Write a story', maxTokens: 4096 })
102
-
103
- // Tool Confirmation (CWE-862)
104
- // BAD: tools: { deleteFile: { execute: async (p) => fs.unlink(p.path) } }
105
- // GOOD: tools: { deleteFile: { requiresConfirmation: true, execute: ... } }
106
- ```
107
-
108
- ## OWASP Reference
109
-
110
- | OWASP LLM Top 10 2025 | Rule |
111
- | ----------------------- | ----------------------------------------- |
112
- | LLM01: Prompt Injection | `require-validated-prompt` |
113
- | LLM02: Sensitive Info | `no-sensitive-in-prompt` |
114
- | LLM05: Output Handling | `no-unsafe-output-handling` |
115
- | LLM06: Excessive Agency | `require-tool-confirmation` |
116
- | LLM10: Unbounded | `require-max-tokens`, `require-max-steps` |
117
-
118
- | OWASP Agentic Top 10 2026 | Rule |
119
- | ------------------------- | --------------------------- |
120
- | ASI02: Tool Misuse | `require-tool-schema` |
121
- | ASI03: Identity Abuse | `no-hardcoded-api-keys` |
122
- | ASI05: Code Execution | `no-unsafe-output-handling` |
123
- | ASI08: Cascading Failures | `require-error-handling` |
124
-
125
- ## Suppression Patterns
126
-
127
- ```typescript
128
- // Suppress single line
129
- // eslint-disable-next-line vercel-ai-security/require-validated-prompt -- Trusted internal input
130
- await generateText({ prompt: internalPrompt });
131
- ```