eslint-plugin-vercel-ai-security 1.4.2 → 1.5.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-vercel-ai-security",
3
- "version": "1.4.2",
3
+ "version": "1.5.0",
4
4
  "description": "ESLint plugin for Vercel AI SDK security — detects prompt injection, system-prompt leaks, hardcoded API keys, and unvalidated model output in generateText and streamText.",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -74,7 +74,8 @@
74
74
  "node": ">=18.0.0"
75
75
  },
76
76
  "peerDependencies": {
77
- "eslint": "^8.0.0 || ^9.0.0 || ^10.0.0"
77
+ "eslint": "^8.0.0 || ^9.0.0 || ^10.0.0",
78
+ "ai": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
78
79
  },
79
80
  "peerDependenciesMeta": {
80
81
  "ai": {
@@ -83,7 +84,7 @@
83
84
  },
84
85
  "dependencies": {
85
86
  "tslib": "^2.3.0",
86
- "@interlace/eslint-devkit": "^1.6.1"
87
+ "@interlace/eslint-devkit": "^1.7.0"
87
88
  },
88
89
  "funding": {
89
90
  "type": "github",
package/src/index.js CHANGED
@@ -82,7 +82,7 @@ exports.rules = {
82
82
  exports.plugin = {
83
83
  meta: {
84
84
  name: 'eslint-plugin-vercel-ai-security',
85
- version: '1.4.2',
85
+ version: '1.5.0',
86
86
  },
87
87
  rules: exports.rules,
88
88
  };
@@ -91,7 +91,16 @@ exports.noUnsafeOutputHandling = (0, eslint_devkit_1.createRule)({
91
91
  const sourceCode = context.sourceCode;
92
92
  const dangerousFunctions = ['eval', 'Function', 'execSync', 'exec', 'spawn', 'execFile'];
93
93
  const sqlPatterns = ['query', 'execute', 'run', 'raw'];
94
- const aiBoundNames = new Set();
94
+ const aiBoundVariables = new Set();
95
+ function isAIBound(node) {
96
+ if (node.type !== 'Identifier')
97
+ return false;
98
+ for (const variable of aiBoundVariables) {
99
+ if (variable.references.some((r) => r.identifier === node))
100
+ return true;
101
+ }
102
+ return false;
103
+ }
95
104
  const AI_SDK_CALLS = new Set(['generateText', 'streamText', 'generateObject', 'streamObject']);
96
105
  function isAISDKCall(node) {
97
106
  let target = node;
@@ -108,29 +117,31 @@ exports.noUnsafeOutputHandling = (0, eslint_devkit_1.createRule)({
108
117
  return false;
109
118
  }
110
119
  function isLikelyAIOutput(node) {
111
- if (node.type === 'MemberExpression' && node.object.type === 'Identifier' &&
112
- aiBoundNames.has(node.object.name)) {
120
+ if (node.type === 'MemberExpression' && isAIBound(node.object)) {
113
121
  return true;
114
122
  }
115
- if (node.type === 'Identifier' && aiBoundNames.has(node.name)) {
123
+ if (isAIBound(node)) {
116
124
  return true;
117
125
  }
118
126
  const text = sourceCode.getText(node);
119
127
  return aiOutputPatterns.some((pattern) => text.includes(pattern));
120
128
  }
129
+ function containsAIOutput(node) {
130
+ if (node.type === 'TemplateLiteral') {
131
+ return node.expressions.some(containsAIOutput);
132
+ }
133
+ if (node.type === 'BinaryExpression') {
134
+ return (node.operator === '+' &&
135
+ (containsAIOutput(node.left) || containsAIOutput(node.right)));
136
+ }
137
+ return isLikelyAIOutput(node);
138
+ }
121
139
  return {
122
140
  VariableDeclarator(node) {
123
141
  if (!node.init || !isAISDKCall(node.init))
124
142
  return;
125
- if (node.id.type === 'Identifier') {
126
- aiBoundNames.add(node.id.name);
127
- }
128
- else if (node.id.type === 'ObjectPattern') {
129
- for (const prop of node.id.properties) {
130
- if (prop.type === 'Property' && prop.value.type === 'Identifier') {
131
- aiBoundNames.add(prop.value.name);
132
- }
133
- }
143
+ for (const variable of sourceCode.getDeclaredVariables(node)) {
144
+ aiBoundVariables.add(variable);
134
145
  }
135
146
  },
136
147
  CallExpression(node) {
@@ -154,7 +165,7 @@ exports.noUnsafeOutputHandling = (0, eslint_devkit_1.createRule)({
154
165
  if (isSQLFunction) {
155
166
  for (const arg of node.arguments) {
156
167
  if (arg.type === 'TemplateLiteral' || arg.type === 'BinaryExpression') {
157
- if (isLikelyAIOutput(arg)) {
168
+ if (containsAIOutput(arg)) {
158
169
  context.report({
159
170
  node: arg,
160
171
  messageId: 'unsafeOutputInSQL',
@@ -76,7 +76,10 @@ exports.requireMaxTokens = (0, eslint_devkit_1.createRule)({
76
76
  : prop.key.type === 'Literal'
77
77
  ? String(prop.key.value)
78
78
  : null;
79
- return keyName === 'maxTokens' || keyName === 'max_tokens' || keyName === 'maxOutputTokens';
79
+ return (keyName === 'maxTokens' ||
80
+ keyName === 'max_tokens' ||
81
+ keyName === 'maxOutputTokens' ||
82
+ keyName === 'max_output_tokens');
80
83
  });
81
84
  if (!hasMaxTokens) {
82
85
  context.report({