ira-review 2.0.1 → 2.0.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.
- package/README.md +13 -49
- package/README.npm.md +13 -49
- package/dist/{bitbucket-TQU3E6SG.js → bitbucket-CSZZGWYR.js} +2 -2
- package/dist/{chunk-NIAFIXU4.js → chunk-4XYBVOZW.js} +11 -10
- package/dist/{chunk-AHCFDKK4.js → chunk-AFLVYFZ2.js} +37 -1
- package/dist/{chunk-XOR6EFZE.js → chunk-RAJNISC2.js} +11 -10
- package/dist/cli.js +169 -43
- package/dist/{github-Q46RU3UR.js → github-XKKZJIZ4.js} +2 -2
- package/dist/index.cjs +192 -48
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +192 -48
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,55 +15,19 @@ No install required. Drop `--dry-run` to post comments directly on the PR. For B
|
|
|
15
15
|
## What You Get
|
|
16
16
|
|
|
17
17
|
```
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
Fix: BEFORE: `db.query(`SELECT * FROM users WHERE name = ${username}`)`
|
|
32
|
-
→ AFTER: `db.query('SELECT * FROM users WHERE name = $1', [username])`
|
|
33
|
-
|
|
34
|
-
────────────────────────────────────────────────────────────
|
|
35
|
-
📄 src/middleware/cors.ts:8
|
|
36
|
-
Rule: IRA/error-handling (MAJOR)
|
|
37
|
-
Message: Empty catch block swallows CORS validation errors
|
|
38
|
-
Explain: fetch() failure in CORS preflight is caught and ignored,
|
|
39
|
-
leaving the request in an undefined state.
|
|
40
|
-
Impact: Silent CORS failures in production with no logging.
|
|
41
|
-
Fix: BEFORE: `} catch {}`
|
|
42
|
-
→ AFTER: `} catch (err) { logger.error('CORS preflight failed', err); throw err; }`
|
|
43
|
-
|
|
44
|
-
# 🔍 IRA Review Summary
|
|
45
|
-
|
|
46
|
-
## 🟡 Risk: MEDIUM (38/100)
|
|
47
|
-
|
|
48
|
-
| Metric | Value |
|
|
49
|
-
|---------------|----------|
|
|
50
|
-
| Review mode | AI-only |
|
|
51
|
-
| Total issues | 3 |
|
|
52
|
-
| Reviewed (AI) | 3 |
|
|
53
|
-
| Framework | react |
|
|
54
|
-
|
|
55
|
-
## ✅ Requirements: AUTH-234 — 83% Complete (5/6)
|
|
56
|
-
|
|
57
|
-
✅ OAuth2 login flow implemented with Google provider
|
|
58
|
-
✅ JWT tokens generated on successful authentication
|
|
59
|
-
✅ Refresh token rotation with 7-day expiry
|
|
60
|
-
❌ Input validation on login endpoint — no email format check
|
|
61
|
-
✅ Logout endpoint clears session and revokes token
|
|
62
|
-
✅ Rate limiting on login attempts
|
|
63
|
-
|
|
64
|
-
⚠️ Edge Cases Not Covered
|
|
65
|
-
- What happens when Google OAuth is unreachable?
|
|
66
|
-
- Token refresh during concurrent requests?
|
|
18
|
+
IRA: Found 3 issues (Risk: MEDIUM - 47/100)
|
|
19
|
+
|
|
20
|
+
src/routes/todos.ts
|
|
21
|
+
[BLOCKER] SQL injection risk - user input passed directly to query
|
|
22
|
+
[MAJOR] Missing database index on frequently queried column
|
|
23
|
+
|
|
24
|
+
src/middleware/auth.ts
|
|
25
|
+
[CRITICAL] JWT secret hardcoded - move to environment variable
|
|
26
|
+
|
|
27
|
+
JIRA AC Validation (PROJ-1234):
|
|
28
|
+
AC 1: User can create a todo item COVERED
|
|
29
|
+
AC 2: Input is validated before save NOT COVERED
|
|
30
|
+
AC 3: Error returns 422 with details COVERED
|
|
67
31
|
```
|
|
68
32
|
|
|
69
33
|
Each issue is posted as an inline comment on the exact PR line with explanation, impact, and a minimal BEFORE → AFTER fix.
|
package/README.npm.md
CHANGED
|
@@ -15,55 +15,19 @@ No install required. Drop `--dry-run` to post comments directly on the PR. For B
|
|
|
15
15
|
## What You Get
|
|
16
16
|
|
|
17
17
|
```
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
Fix: BEFORE: `db.query(`SELECT * FROM users WHERE name = ${username}`)`
|
|
32
|
-
→ AFTER: `db.query('SELECT * FROM users WHERE name = $1', [username])`
|
|
33
|
-
|
|
34
|
-
────────────────────────────────────────────────────────────
|
|
35
|
-
📄 src/middleware/cors.ts:8
|
|
36
|
-
Rule: IRA/error-handling (MAJOR)
|
|
37
|
-
Message: Empty catch block swallows CORS validation errors
|
|
38
|
-
Explain: fetch() failure in CORS preflight is caught and ignored,
|
|
39
|
-
leaving the request in an undefined state.
|
|
40
|
-
Impact: Silent CORS failures in production with no logging.
|
|
41
|
-
Fix: BEFORE: `} catch {}`
|
|
42
|
-
→ AFTER: `} catch (err) { logger.error('CORS preflight failed', err); throw err; }`
|
|
43
|
-
|
|
44
|
-
# 🔍 IRA Review Summary
|
|
45
|
-
|
|
46
|
-
## 🟡 Risk: MEDIUM (38/100)
|
|
47
|
-
|
|
48
|
-
| Metric | Value |
|
|
49
|
-
|---------------|----------|
|
|
50
|
-
| Review mode | AI-only |
|
|
51
|
-
| Total issues | 3 |
|
|
52
|
-
| Reviewed (AI) | 3 |
|
|
53
|
-
| Framework | react |
|
|
54
|
-
|
|
55
|
-
## ✅ Requirements: AUTH-234 — 83% Complete (5/6)
|
|
56
|
-
|
|
57
|
-
✅ OAuth2 login flow implemented with Google provider
|
|
58
|
-
✅ JWT tokens generated on successful authentication
|
|
59
|
-
✅ Refresh token rotation with 7-day expiry
|
|
60
|
-
❌ Input validation on login endpoint — no email format check
|
|
61
|
-
✅ Logout endpoint clears session and revokes token
|
|
62
|
-
✅ Rate limiting on login attempts
|
|
63
|
-
|
|
64
|
-
⚠️ Edge Cases Not Covered
|
|
65
|
-
- What happens when Google OAuth is unreachable?
|
|
66
|
-
- Token refresh during concurrent requests?
|
|
18
|
+
IRA: Found 3 issues (Risk: MEDIUM - 47/100)
|
|
19
|
+
|
|
20
|
+
src/routes/todos.ts
|
|
21
|
+
[BLOCKER] SQL injection risk - user input passed directly to query
|
|
22
|
+
[MAJOR] Missing database index on frequently queried column
|
|
23
|
+
|
|
24
|
+
src/middleware/auth.ts
|
|
25
|
+
[CRITICAL] JWT secret hardcoded - move to environment variable
|
|
26
|
+
|
|
27
|
+
JIRA AC Validation (PROJ-1234):
|
|
28
|
+
AC 1: User can create a todo item COVERED
|
|
29
|
+
AC 2: Input is validated before save NOT COVERED
|
|
30
|
+
AC 3: Error returns 422 with details COVERED
|
|
67
31
|
```
|
|
68
32
|
|
|
69
33
|
Each issue is posted as an inline comment on the exact PR line with explanation, impact, and a minimal BEFORE → AFTER fix.
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
import {
|
|
3
3
|
RetryableError,
|
|
4
4
|
fetchWithTimeout,
|
|
5
|
+
parseApiError,
|
|
5
6
|
withRetry
|
|
6
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-AFLVYFZ2.js";
|
|
7
8
|
|
|
8
9
|
// src/scm/github.ts
|
|
9
10
|
var GitHubClient = class {
|
|
@@ -48,7 +49,7 @@ var GitHubClient = class {
|
|
|
48
49
|
if (!response.ok) {
|
|
49
50
|
const text = await response.text();
|
|
50
51
|
throw new RetryableError(
|
|
51
|
-
|
|
52
|
+
parseApiError(response.status, text, "GitHub"),
|
|
52
53
|
response.status
|
|
53
54
|
);
|
|
54
55
|
}
|
|
@@ -81,7 +82,7 @@ var GitHubClient = class {
|
|
|
81
82
|
if (!response.ok) {
|
|
82
83
|
const text = await response.text();
|
|
83
84
|
throw new RetryableError(
|
|
84
|
-
|
|
85
|
+
parseApiError(response.status, text, "GitHub"),
|
|
85
86
|
response.status
|
|
86
87
|
);
|
|
87
88
|
}
|
|
@@ -97,7 +98,7 @@ var GitHubClient = class {
|
|
|
97
98
|
const response = await fetchWithTimeout(url, { headers: this.headers });
|
|
98
99
|
if (!response.ok) {
|
|
99
100
|
const text = await response.text();
|
|
100
|
-
throw new RetryableError(
|
|
101
|
+
throw new RetryableError(parseApiError(response.status, text, "GitHub"), response.status);
|
|
101
102
|
}
|
|
102
103
|
return response.json();
|
|
103
104
|
});
|
|
@@ -125,7 +126,7 @@ ${file.patch}`;
|
|
|
125
126
|
if (!response.ok) {
|
|
126
127
|
const text = await response.text();
|
|
127
128
|
throw new RetryableError(
|
|
128
|
-
|
|
129
|
+
parseApiError(response.status, text, "GitHub"),
|
|
129
130
|
response.status
|
|
130
131
|
);
|
|
131
132
|
}
|
|
@@ -154,7 +155,7 @@ ${file.patch}`;
|
|
|
154
155
|
if (!response.ok) {
|
|
155
156
|
const text = await response.text();
|
|
156
157
|
throw new RetryableError(
|
|
157
|
-
|
|
158
|
+
parseApiError(response.status, text, "GitHub"),
|
|
158
159
|
response.status
|
|
159
160
|
);
|
|
160
161
|
}
|
|
@@ -171,7 +172,7 @@ ${file.patch}`;
|
|
|
171
172
|
if (!response.ok) {
|
|
172
173
|
const text = await response.text();
|
|
173
174
|
throw new RetryableError(
|
|
174
|
-
|
|
175
|
+
parseApiError(response.status, text, "GitHub"),
|
|
175
176
|
response.status
|
|
176
177
|
);
|
|
177
178
|
}
|
|
@@ -188,7 +189,7 @@ ${file.patch}`;
|
|
|
188
189
|
if (!response.ok) {
|
|
189
190
|
const text = await response.text();
|
|
190
191
|
throw new RetryableError(
|
|
191
|
-
|
|
192
|
+
parseApiError(response.status, text, "GitHub"),
|
|
192
193
|
response.status
|
|
193
194
|
);
|
|
194
195
|
}
|
|
@@ -221,7 +222,7 @@ ${file.patch}`;
|
|
|
221
222
|
if (!response.ok && response.status !== 422) {
|
|
222
223
|
const text = await response.text();
|
|
223
224
|
throw new RetryableError(
|
|
224
|
-
|
|
225
|
+
parseApiError(response.status, text, "GitHub"),
|
|
225
226
|
response.status
|
|
226
227
|
);
|
|
227
228
|
}
|
|
@@ -258,7 +259,7 @@ ${file.patch}`;
|
|
|
258
259
|
if (!response.ok) {
|
|
259
260
|
const text = await response.text();
|
|
260
261
|
throw new RetryableError(
|
|
261
|
-
|
|
262
|
+
parseApiError(response.status, text, "GitHub"),
|
|
262
263
|
response.status
|
|
263
264
|
);
|
|
264
265
|
}
|
|
@@ -88,6 +88,41 @@ async function fetchWithTimeout(url, init = {}, timeoutMs = 3e4) {
|
|
|
88
88
|
clearTimeout(timer);
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
|
+
function parseApiError(status, body, provider) {
|
|
92
|
+
const statusMessages = {
|
|
93
|
+
400: "Bad request",
|
|
94
|
+
401: "Authentication failed \u2014 check your token or credentials",
|
|
95
|
+
403: "Access denied \u2014 you may not have permission for this resource",
|
|
96
|
+
404: "Not found \u2014 check the URL, project key, or PR number",
|
|
97
|
+
408: "Request timed out \u2014 try again in a moment",
|
|
98
|
+
409: "Conflict \u2014 the resource may have been modified",
|
|
99
|
+
422: "Invalid request \u2014 the server couldn't process it",
|
|
100
|
+
429: "Rate limited \u2014 too many requests, try again shortly",
|
|
101
|
+
500: "Server error \u2014 the service is having issues",
|
|
102
|
+
502: "Bad gateway \u2014 the service is temporarily unavailable",
|
|
103
|
+
503: "Service unavailable \u2014 try again in a moment",
|
|
104
|
+
504: "Gateway timeout \u2014 the service took too long to respond"
|
|
105
|
+
};
|
|
106
|
+
const friendlyStatus = statusMessages[status] ?? `HTTP ${status}`;
|
|
107
|
+
const trimmed = body.trim();
|
|
108
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
109
|
+
try {
|
|
110
|
+
const json = JSON.parse(trimmed);
|
|
111
|
+
const msg = json.message ?? json.error?.message ?? json.error ?? json.errors?.[0]?.message ?? json.detail;
|
|
112
|
+
if (typeof msg === "string" && msg.length > 0 && msg.length < 200) {
|
|
113
|
+
return `${provider} (${status}): ${msg}`;
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (trimmed.startsWith("<!") || trimmed.startsWith("<html") || trimmed.includes("<body")) {
|
|
119
|
+
return `${provider} (${status}): ${friendlyStatus}`;
|
|
120
|
+
}
|
|
121
|
+
if (trimmed.length > 0 && trimmed.length < 150) {
|
|
122
|
+
return `${provider} (${status}): ${trimmed}`;
|
|
123
|
+
}
|
|
124
|
+
return `${provider} (${status}): ${friendlyStatus}`;
|
|
125
|
+
}
|
|
91
126
|
function sleep(ms) {
|
|
92
127
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
93
128
|
}
|
|
@@ -95,5 +130,6 @@ function sleep(ms) {
|
|
|
95
130
|
export {
|
|
96
131
|
RetryableError,
|
|
97
132
|
withRetry,
|
|
98
|
-
fetchWithTimeout
|
|
133
|
+
fetchWithTimeout,
|
|
134
|
+
parseApiError
|
|
99
135
|
};
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
import {
|
|
3
3
|
RetryableError,
|
|
4
4
|
fetchWithTimeout,
|
|
5
|
+
parseApiError,
|
|
5
6
|
withRetry
|
|
6
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-AFLVYFZ2.js";
|
|
7
8
|
|
|
8
9
|
// src/scm/bitbucket.ts
|
|
9
10
|
var BitbucketClient = class {
|
|
@@ -55,14 +56,14 @@ var BitbucketClient = class {
|
|
|
55
56
|
if (!retryResponse.ok) {
|
|
56
57
|
const retryText = await retryResponse.text();
|
|
57
58
|
throw new RetryableError(
|
|
58
|
-
|
|
59
|
+
parseApiError(retryResponse.status, retryText, "Bitbucket"),
|
|
59
60
|
retryResponse.status
|
|
60
61
|
);
|
|
61
62
|
}
|
|
62
63
|
return;
|
|
63
64
|
}
|
|
64
65
|
throw new RetryableError(
|
|
65
|
-
|
|
66
|
+
parseApiError(response.status, text, "Bitbucket"),
|
|
66
67
|
response.status
|
|
67
68
|
);
|
|
68
69
|
}
|
|
@@ -82,7 +83,7 @@ var BitbucketClient = class {
|
|
|
82
83
|
if (!response.ok) {
|
|
83
84
|
const text = await response.text();
|
|
84
85
|
throw new RetryableError(
|
|
85
|
-
|
|
86
|
+
parseApiError(response.status, text, "Bitbucket"),
|
|
86
87
|
response.status
|
|
87
88
|
);
|
|
88
89
|
}
|
|
@@ -99,7 +100,7 @@ var BitbucketClient = class {
|
|
|
99
100
|
if (!response.ok) {
|
|
100
101
|
const text = await response.text();
|
|
101
102
|
throw new RetryableError(
|
|
102
|
-
|
|
103
|
+
parseApiError(response.status, text, "Bitbucket"),
|
|
103
104
|
response.status
|
|
104
105
|
);
|
|
105
106
|
}
|
|
@@ -131,7 +132,7 @@ var BitbucketClient = class {
|
|
|
131
132
|
if (!response.ok) {
|
|
132
133
|
const text = await response.text();
|
|
133
134
|
throw new RetryableError(
|
|
134
|
-
|
|
135
|
+
parseApiError(response.status, text, "Bitbucket"),
|
|
135
136
|
response.status
|
|
136
137
|
);
|
|
137
138
|
}
|
|
@@ -147,7 +148,7 @@ var BitbucketClient = class {
|
|
|
147
148
|
const response = await fetchWithTimeout(nextUrl, { headers: this.headers });
|
|
148
149
|
if (!response.ok) {
|
|
149
150
|
const text = await response.text();
|
|
150
|
-
throw new RetryableError(
|
|
151
|
+
throw new RetryableError(parseApiError(response.status, text, "Bitbucket"), response.status);
|
|
151
152
|
}
|
|
152
153
|
return response.json();
|
|
153
154
|
});
|
|
@@ -166,7 +167,7 @@ var BitbucketClient = class {
|
|
|
166
167
|
const response = await fetchWithTimeout(url, { headers: this.headers }, 15e3);
|
|
167
168
|
if (!response.ok) {
|
|
168
169
|
const text = await response.text();
|
|
169
|
-
throw new RetryableError(
|
|
170
|
+
throw new RetryableError(parseApiError(response.status, text, "Bitbucket"), response.status);
|
|
170
171
|
}
|
|
171
172
|
return response.text();
|
|
172
173
|
});
|
|
@@ -189,7 +190,7 @@ var BitbucketClient = class {
|
|
|
189
190
|
if (!response.ok) {
|
|
190
191
|
const text = await response.text();
|
|
191
192
|
throw new RetryableError(
|
|
192
|
-
|
|
193
|
+
parseApiError(response.status, text, "Bitbucket"),
|
|
193
194
|
response.status
|
|
194
195
|
);
|
|
195
196
|
}
|
|
@@ -218,7 +219,7 @@ var BitbucketClient = class {
|
|
|
218
219
|
if (!response.ok) {
|
|
219
220
|
const text = await response.text();
|
|
220
221
|
throw new RetryableError(
|
|
221
|
-
|
|
222
|
+
parseApiError(response.status, text, "Bitbucket"),
|
|
222
223
|
response.status
|
|
223
224
|
);
|
|
224
225
|
}
|