@firela/billclaw-core 0.2.0 → 0.4.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/dist/billclaw.d.ts.map +1 -1
- package/dist/billclaw.js +38 -6
- package/dist/billclaw.js.map +1 -1
- package/dist/errors/errors.d.ts +110 -11
- package/dist/errors/errors.d.ts.map +1 -1
- package/dist/errors/errors.js +421 -122
- package/dist/errors/errors.js.map +1 -1
- package/dist/sources/gmail/gmail-fetch.d.ts +2 -2
- package/dist/sources/gmail/gmail-fetch.d.ts.map +1 -1
- package/dist/sources/gmail/gmail-fetch.js +34 -4
- package/dist/sources/gmail/gmail-fetch.js.map +1 -1
- package/dist/sources/plaid/plaid-sync.d.ts +2 -2
- package/dist/sources/plaid/plaid-sync.d.ts.map +1 -1
- package/dist/sources/plaid/plaid-sync.js +78 -18
- package/dist/sources/plaid/plaid-sync.js.map +1 -1
- package/dist/sync/sync-service.d.ts +2 -2
- package/dist/sync/sync-service.d.ts.map +1 -1
- package/dist/sync/sync-service.js +6 -1
- package/dist/sync/sync-service.js.map +1 -1
- package/package.json +1 -1
package/dist/errors/errors.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Error handling utilities for BillClaw
|
|
3
3
|
*
|
|
4
|
-
* Provides
|
|
5
|
-
*
|
|
4
|
+
* Provides dual-mode error handling:
|
|
5
|
+
* - Machine-readable: For AI agents (error codes, severity, executable actions)
|
|
6
|
+
* - Human-readable: For display (title, message, suggestions)
|
|
6
7
|
*
|
|
7
8
|
* Framework-agnostic: Logger is provided via runtime abstraction.
|
|
8
9
|
*/
|
|
@@ -29,45 +30,121 @@ export var ErrorCategory;
|
|
|
29
30
|
ErrorCategory["UNKNOWN"] = "unknown";
|
|
30
31
|
})(ErrorCategory || (ErrorCategory = {}));
|
|
31
32
|
/**
|
|
32
|
-
*
|
|
33
|
+
* Error codes for programmatic error handling
|
|
34
|
+
* Used by AI agents for switch/case logic and decision making
|
|
33
35
|
*/
|
|
34
|
-
export
|
|
36
|
+
export const ERROR_CODES = {
|
|
37
|
+
// Lock errors
|
|
38
|
+
LOCK_ACQUISITION_FAILED: "LOCK_ACQUISITION_FAILED",
|
|
39
|
+
LOCK_TIMEOUT: "LOCK_TIMEOUT",
|
|
40
|
+
LOCK_STALE: "LOCK_STALE",
|
|
41
|
+
// Credential errors
|
|
42
|
+
CREDENTIALS_NOT_FOUND: "CREDENTIALS_NOT_FOUND",
|
|
43
|
+
CREDENTIALS_STORAGE_FAILED: "CREDENTIALS_STORAGE_FAILED",
|
|
44
|
+
CREDENTIALS_KEYCHAIN_FAILED: "CREDENTIALS_KEYCHAIN_FAILED",
|
|
45
|
+
// Network errors
|
|
46
|
+
NETWORK_CONNECTION_REFUSED: "NETWORK_CONNECTION_REFUSED",
|
|
47
|
+
NETWORK_TIMEOUT: "NETWORK_TIMEOUT",
|
|
48
|
+
NETWORK_DNS_FAILED: "NETWORK_DNS_FAILED",
|
|
49
|
+
NETWORK_GENERIC: "NETWORK_GENERIC",
|
|
50
|
+
// Plaid errors
|
|
51
|
+
PLAID_ITEM_LOGIN_REQUIRED: "PLAID_ITEM_LOGIN_REQUIRED",
|
|
52
|
+
PLAID_INVALID_ACCESS_TOKEN: "PLAID_INVALID_ACCESS_TOKEN",
|
|
53
|
+
PLAID_PRODUCT_NOT_READY: "PLAID_PRODUCT_NOT_READY",
|
|
54
|
+
PLAID_RATE_LIMIT_EXCEEDED: "PLAID_RATE_LIMIT_EXCEEDED",
|
|
55
|
+
PLAID_INSTITUTION_DOWN: "PLAID_INSTITUTION_DOWN",
|
|
56
|
+
PLAID_INVALID_CREDENTIALS: "PLAID_INVALID_CREDENTIALS",
|
|
57
|
+
PLAID_API_ERROR: "PLAID_API_ERROR",
|
|
58
|
+
// Gmail errors
|
|
59
|
+
GMAIL_AUTH_FAILED: "GMAIL_AUTH_FAILED",
|
|
60
|
+
GMAIL_ACCESS_DENIED: "GMAIL_ACCESS_DENIED",
|
|
61
|
+
GMAIL_API_NOT_FOUND: "GMAIL_API_NOT_FOUND",
|
|
62
|
+
GMAIL_RATE_LIMIT_EXCEEDED: "GMAIL_RATE_LIMIT_EXCEEDED",
|
|
63
|
+
GMAIL_API_ERROR: "GMAIL_API_ERROR",
|
|
64
|
+
// Storage errors
|
|
65
|
+
STORAGE_DISK_FULL: "STORAGE_DISK_FULL",
|
|
66
|
+
STORAGE_WRITE_FAILED: "STORAGE_WRITE_FAILED",
|
|
67
|
+
STORAGE_READ_FAILED: "STORAGE_READ_FAILED",
|
|
68
|
+
// File system errors
|
|
69
|
+
FS_PERMISSION_DENIED: "FS_PERMISSION_DENIED",
|
|
70
|
+
FS_NOT_FOUND: "FS_NOT_FOUND",
|
|
71
|
+
FS_GENERIC: "FS_GENERIC",
|
|
72
|
+
// Config errors
|
|
73
|
+
CONFIG_INVALID: "CONFIG_INVALID",
|
|
74
|
+
CONFIG_MISSING: "CONFIG_MISSING",
|
|
75
|
+
CONFIG_PARSE_FAILED: "CONFIG_PARSE_FAILED",
|
|
76
|
+
// Generic
|
|
77
|
+
UNKNOWN_ERROR: "UNKNOWN_ERROR",
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Create a user-friendly error with dual-mode support
|
|
81
|
+
*
|
|
82
|
+
* @param errorCode - Machine-readable error code
|
|
83
|
+
* @param category - Error category
|
|
84
|
+
* @param severity - Error severity level
|
|
85
|
+
* @param recoverable - Whether the error is recoverable
|
|
86
|
+
* @param humanReadable - Human-readable error information
|
|
87
|
+
* @param nextActions - AI-executable recovery actions
|
|
88
|
+
* @param entities - Structured entity references
|
|
89
|
+
* @param originalError - Original error for debugging
|
|
90
|
+
*/
|
|
91
|
+
export function createUserError(errorCode, category, severity, recoverable, humanReadable, nextActions, entities, originalError) {
|
|
35
92
|
return {
|
|
36
93
|
type: "UserError",
|
|
94
|
+
errorCode,
|
|
37
95
|
category,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
96
|
+
severity,
|
|
97
|
+
recoverable,
|
|
98
|
+
humanReadable,
|
|
99
|
+
nextActions,
|
|
100
|
+
entities,
|
|
101
|
+
originalError,
|
|
102
|
+
timestamp: new Date().toISOString(),
|
|
43
103
|
};
|
|
44
104
|
}
|
|
45
105
|
/**
|
|
46
106
|
* Format error for display to user
|
|
107
|
+
* Uses the humanReadable field for user-friendly output
|
|
47
108
|
*/
|
|
48
109
|
export function formatError(error) {
|
|
49
110
|
const lines = [];
|
|
50
|
-
// Header with category emoji
|
|
111
|
+
// Header with category emoji and severity
|
|
51
112
|
const categoryEmoji = getCategoryEmoji(error.category);
|
|
52
|
-
|
|
113
|
+
const severityIndicator = getSeverityIndicator(error.severity);
|
|
114
|
+
lines.push(`${categoryEmoji} ${error.humanReadable.title} ${severityIndicator}`);
|
|
53
115
|
lines.push("");
|
|
54
116
|
// Message
|
|
55
|
-
lines.push(error.message);
|
|
117
|
+
lines.push(error.humanReadable.message);
|
|
56
118
|
lines.push("");
|
|
57
119
|
// Suggestions
|
|
58
|
-
if (error.suggestions.length > 0) {
|
|
120
|
+
if (error.humanReadable.suggestions.length > 0) {
|
|
59
121
|
lines.push("Suggestions:");
|
|
60
|
-
for (let i = 0; i < error.suggestions.length; i++) {
|
|
61
|
-
lines.push(` ${i + 1}. ${error.suggestions[i]}`);
|
|
122
|
+
for (let i = 0; i < error.humanReadable.suggestions.length; i++) {
|
|
123
|
+
lines.push(` ${i + 1}. ${error.humanReadable.suggestions[i]}`);
|
|
62
124
|
}
|
|
63
125
|
}
|
|
64
126
|
// Docs link
|
|
65
|
-
if (error.docsLink) {
|
|
127
|
+
if (error.humanReadable.docsLink) {
|
|
66
128
|
lines.push("");
|
|
67
|
-
lines.push(`Learn more: ${error.docsLink}`);
|
|
129
|
+
lines.push(`Learn more: ${error.humanReadable.docsLink}`);
|
|
68
130
|
}
|
|
131
|
+
// Error code (for debugging/Support)
|
|
132
|
+
lines.push("");
|
|
133
|
+
lines.push(`Error code: ${error.errorCode}`);
|
|
69
134
|
return lines.join("\n");
|
|
70
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* Get severity indicator for display
|
|
138
|
+
*/
|
|
139
|
+
function getSeverityIndicator(severity) {
|
|
140
|
+
const indicators = {
|
|
141
|
+
fatal: "🔴",
|
|
142
|
+
error: "❌",
|
|
143
|
+
warning: "⚠️",
|
|
144
|
+
info: "ℹ️",
|
|
145
|
+
};
|
|
146
|
+
return indicators[severity] || "";
|
|
147
|
+
}
|
|
71
148
|
/**
|
|
72
149
|
* Get emoji for error category
|
|
73
150
|
*/
|
|
@@ -90,107 +167,245 @@ function getCategoryEmoji(category) {
|
|
|
90
167
|
/**
|
|
91
168
|
* Parse Plaid error codes and create user-friendly errors
|
|
92
169
|
*/
|
|
93
|
-
export function parsePlaidError(error) {
|
|
170
|
+
export function parsePlaidError(error, accountId) {
|
|
94
171
|
const errorCode = error.error_code || "UNKNOWN";
|
|
95
172
|
const errorMessage = error.error_message || error.display_message || "An error occurred";
|
|
96
173
|
const requestId = error.request_id;
|
|
97
174
|
// Login required
|
|
98
175
|
if (errorCode === "ITEM_LOGIN_REQUIRED" ||
|
|
99
176
|
error.error_type === "ITEM_LOGIN_REQUIRED") {
|
|
100
|
-
return createUserError(ErrorCategory.PLAID_AUTH, "
|
|
101
|
-
"Re-
|
|
102
|
-
"
|
|
103
|
-
|
|
104
|
-
|
|
177
|
+
return createUserError(ERROR_CODES.PLAID_ITEM_LOGIN_REQUIRED, ErrorCategory.PLAID_AUTH, "error", true, {
|
|
178
|
+
title: "Account Re-Authentication Required",
|
|
179
|
+
message: "Your bank account requires re-authentication. This happens when your bank credentials have changed or expired.",
|
|
180
|
+
suggestions: [
|
|
181
|
+
"Re-authenticate via your adapter's setup command",
|
|
182
|
+
"This will open a secure browser window where you can log into your bank",
|
|
183
|
+
"After re-authentication, your transactions will sync normally",
|
|
184
|
+
],
|
|
185
|
+
docsLink: "https://plaid.com/docs/errors/#item-login-required",
|
|
186
|
+
}, [
|
|
187
|
+
{
|
|
188
|
+
type: "oauth_reauth",
|
|
189
|
+
tool: "plaid_oauth",
|
|
190
|
+
params: { accountId, item_id: error.item_id },
|
|
191
|
+
description: "Trigger OAuth re-authentication flow",
|
|
192
|
+
},
|
|
193
|
+
], { accountId, itemId: error.item_id, institutionId: error.institution_id });
|
|
105
194
|
}
|
|
106
195
|
// Invalid credentials
|
|
107
196
|
if (errorCode === "INVALID_ACCESS_TOKEN" ||
|
|
108
197
|
error.error_type === "INVALID_ACCESS_TOKEN") {
|
|
109
|
-
return createUserError(ErrorCategory.PLAID_AUTH, "
|
|
110
|
-
"
|
|
111
|
-
"
|
|
112
|
-
|
|
198
|
+
return createUserError(ERROR_CODES.PLAID_INVALID_ACCESS_TOKEN, ErrorCategory.PLAID_AUTH, "error", true, {
|
|
199
|
+
title: "Invalid Access Token",
|
|
200
|
+
message: "Your access token is invalid. This can happen if the token was revoked or corrupted.",
|
|
201
|
+
suggestions: [
|
|
202
|
+
"Run your adapter's setup command to reconnect your account",
|
|
203
|
+
"If this persists, remove and re-add the account",
|
|
204
|
+
],
|
|
205
|
+
docsLink: "https://plaid.com/docs/errors/#invalid-access-token",
|
|
206
|
+
}, [
|
|
207
|
+
{
|
|
208
|
+
type: "oauth_reauth",
|
|
209
|
+
tool: "plaid_oauth",
|
|
210
|
+
params: { accountId },
|
|
211
|
+
description: "Re-authenticate to get a new access token",
|
|
212
|
+
},
|
|
213
|
+
], { accountId, itemId: error.item_id });
|
|
113
214
|
}
|
|
114
215
|
// Product not ready
|
|
115
216
|
if (errorCode === "PRODUCT_NOT_READY") {
|
|
116
|
-
return createUserError(ErrorCategory.PLAID_API, "
|
|
117
|
-
"
|
|
118
|
-
"
|
|
119
|
-
|
|
217
|
+
return createUserError(ERROR_CODES.PLAID_PRODUCT_NOT_READY, ErrorCategory.PLAID_API, "warning", true, {
|
|
218
|
+
title: "Account Not Ready",
|
|
219
|
+
message: "Your account is not fully set up yet. Plaid is still processing your account information.",
|
|
220
|
+
suggestions: [
|
|
221
|
+
"Wait a few minutes and try again",
|
|
222
|
+
"If this persists, contact Plaid support",
|
|
223
|
+
],
|
|
224
|
+
docsLink: "https://plaid.com/docs/errors/#product-not-ready",
|
|
225
|
+
}, [
|
|
226
|
+
{
|
|
227
|
+
type: "retry",
|
|
228
|
+
delayMs: 60000,
|
|
229
|
+
description: "Retry after 1 minute",
|
|
230
|
+
},
|
|
231
|
+
], { accountId, itemId: error.item_id });
|
|
120
232
|
}
|
|
121
233
|
// Rate limit
|
|
122
234
|
if (errorCode === "RATE_LIMIT_EXCEEDED") {
|
|
123
|
-
return createUserError(ErrorCategory.PLAID_API, "
|
|
124
|
-
"
|
|
125
|
-
"
|
|
126
|
-
|
|
127
|
-
|
|
235
|
+
return createUserError(ERROR_CODES.PLAID_RATE_LIMIT_EXCEEDED, ErrorCategory.PLAID_API, "warning", true, {
|
|
236
|
+
title: "API Rate Limit Exceeded",
|
|
237
|
+
message: "Too many requests have been made to the Plaid API. Please wait before trying again.",
|
|
238
|
+
suggestions: [
|
|
239
|
+
"Wait a few minutes before syncing again",
|
|
240
|
+
"Consider syncing less frequently (e.g., daily instead of hourly)",
|
|
241
|
+
"If you need higher rate limits, upgrade your Plaid plan",
|
|
242
|
+
],
|
|
243
|
+
docsLink: "https://plaid.com/docs/errors/#rate-limit-exceeded",
|
|
244
|
+
}, [
|
|
245
|
+
{
|
|
246
|
+
type: "retry",
|
|
247
|
+
delayMs: 300000, // 5 minutes
|
|
248
|
+
description: "Retry after 5 minutes",
|
|
249
|
+
},
|
|
250
|
+
], { accountId });
|
|
128
251
|
}
|
|
129
252
|
// Institution down
|
|
130
253
|
if (errorCode === "INSTITUTION_DOWN") {
|
|
131
|
-
return createUserError(ErrorCategory.PLAID_API, "
|
|
132
|
-
"
|
|
133
|
-
"
|
|
134
|
-
|
|
135
|
-
|
|
254
|
+
return createUserError(ERROR_CODES.PLAID_INSTITUTION_DOWN, ErrorCategory.PLAID_API, "warning", true, {
|
|
255
|
+
title: "Bank temporarily unavailable",
|
|
256
|
+
message: "Your bank's systems are temporarily down for maintenance.",
|
|
257
|
+
suggestions: [
|
|
258
|
+
"Wait a few minutes and try again",
|
|
259
|
+
"Check your bank's website for service status updates",
|
|
260
|
+
"Your transactions will sync automatically once the bank is back online",
|
|
261
|
+
],
|
|
262
|
+
}, [
|
|
263
|
+
{
|
|
264
|
+
type: "retry",
|
|
265
|
+
delayMs: 300000, // 5 minutes
|
|
266
|
+
description: "Retry after 5 minutes",
|
|
267
|
+
},
|
|
268
|
+
], {
|
|
269
|
+
accountId,
|
|
270
|
+
institutionId: error.institution_id,
|
|
271
|
+
});
|
|
136
272
|
}
|
|
137
273
|
// Invalid credentials
|
|
138
274
|
if (errorCode === "INVALID_CREDENTIALS") {
|
|
139
|
-
return createUserError(ErrorCategory.PLAID_API, "
|
|
140
|
-
"
|
|
141
|
-
"
|
|
142
|
-
|
|
275
|
+
return createUserError(ERROR_CODES.PLAID_INVALID_CREDENTIALS, ErrorCategory.PLAID_API, "error", false, {
|
|
276
|
+
title: "Invalid API Credentials",
|
|
277
|
+
message: "The Plaid API credentials configured are invalid.",
|
|
278
|
+
suggestions: [
|
|
279
|
+
"Configure your Plaid client ID and secret",
|
|
280
|
+
"Verify your credentials at https://dashboard.plaid.com",
|
|
281
|
+
],
|
|
282
|
+
docsLink: "https://dashboard.plaid.com",
|
|
283
|
+
}, [
|
|
284
|
+
{
|
|
285
|
+
type: "config_change",
|
|
286
|
+
params: { setting: "plaid_credentials" },
|
|
287
|
+
description: "Update Plaid API credentials",
|
|
288
|
+
},
|
|
289
|
+
], {});
|
|
143
290
|
}
|
|
144
291
|
// Generic Plaid error
|
|
145
|
-
return createUserError(ErrorCategory.PLAID_API, "
|
|
146
|
-
"
|
|
147
|
-
|
|
148
|
-
|
|
292
|
+
return createUserError(ERROR_CODES.PLAID_API_ERROR, ErrorCategory.PLAID_API, "error", true, {
|
|
293
|
+
title: "Plaid API Error",
|
|
294
|
+
message: `${errorMessage}${requestId ? ` (Request ID: ${requestId})` : ""}`,
|
|
295
|
+
suggestions: [
|
|
296
|
+
"Try again in a few minutes",
|
|
297
|
+
"Check Plaid status at https://status.plaid.com",
|
|
298
|
+
],
|
|
299
|
+
docsLink: "https://plaid.com/docs/errors/",
|
|
300
|
+
}, [
|
|
301
|
+
{
|
|
302
|
+
type: "retry",
|
|
303
|
+
delayMs: 60000,
|
|
304
|
+
description: "Retry after 1 minute",
|
|
305
|
+
},
|
|
306
|
+
], { accountId, itemId: error.item_id });
|
|
149
307
|
}
|
|
150
308
|
/**
|
|
151
309
|
* Parse Gmail API errors and create user-friendly errors
|
|
152
310
|
*/
|
|
153
|
-
export function parseGmailError(error) {
|
|
311
|
+
export function parseGmailError(error, accountId) {
|
|
154
312
|
const statusCode = error.status || error.code || 0;
|
|
155
313
|
// Unauthorized
|
|
156
314
|
if (statusCode === 401) {
|
|
157
|
-
return createUserError(ErrorCategory.GMAIL_AUTH, "
|
|
158
|
-
"
|
|
159
|
-
"
|
|
160
|
-
|
|
161
|
-
|
|
315
|
+
return createUserError(ERROR_CODES.GMAIL_AUTH_FAILED, ErrorCategory.GMAIL_AUTH, "error", true, {
|
|
316
|
+
title: "Gmail Authentication Failed",
|
|
317
|
+
message: "Your Gmail access has expired or been revoked. You need to re-authenticate.",
|
|
318
|
+
suggestions: [
|
|
319
|
+
"Re-authenticate with Gmail via your adapter's setup command",
|
|
320
|
+
"Make sure you grant read-only access to your Gmail",
|
|
321
|
+
"Check that Google Cloud OAuth credentials are valid",
|
|
322
|
+
],
|
|
323
|
+
docsLink: "https://developers.google.com/gmail/api/auth",
|
|
324
|
+
}, [
|
|
325
|
+
{
|
|
326
|
+
type: "oauth_reauth",
|
|
327
|
+
tool: "gmail_oauth",
|
|
328
|
+
params: { accountId },
|
|
329
|
+
description: "Re-authenticate with Gmail",
|
|
330
|
+
},
|
|
331
|
+
], { accountId });
|
|
162
332
|
}
|
|
163
333
|
// Forbidden
|
|
164
334
|
if (statusCode === 403) {
|
|
165
|
-
return createUserError(ErrorCategory.GMAIL_API, "
|
|
166
|
-
"
|
|
167
|
-
"
|
|
168
|
-
|
|
169
|
-
|
|
335
|
+
return createUserError(ERROR_CODES.GMAIL_ACCESS_DENIED, ErrorCategory.GMAIL_API, "error", false, {
|
|
336
|
+
title: "Gmail Access Denied",
|
|
337
|
+
message: "Access to Gmail was denied. This usually means the OAuth token lacks the required permissions.",
|
|
338
|
+
suggestions: [
|
|
339
|
+
"Make sure the Gmail API is enabled in Google Cloud Console",
|
|
340
|
+
"Verify that the OAuth consent screen includes 'gmail.readonly' scope",
|
|
341
|
+
"Re-authenticate to grant proper permissions",
|
|
342
|
+
],
|
|
343
|
+
docsLink: "https://developers.google.com/gmail/api/auth",
|
|
344
|
+
}, [
|
|
345
|
+
{
|
|
346
|
+
type: "oauth_reauth",
|
|
347
|
+
tool: "gmail_oauth",
|
|
348
|
+
params: { accountId },
|
|
349
|
+
description: "Re-authenticate with proper permissions",
|
|
350
|
+
},
|
|
351
|
+
], { accountId });
|
|
170
352
|
}
|
|
171
353
|
// Not found
|
|
172
354
|
if (statusCode === 404) {
|
|
173
|
-
return createUserError(ErrorCategory.GMAIL_API, "
|
|
174
|
-
"
|
|
175
|
-
"
|
|
176
|
-
|
|
177
|
-
|
|
355
|
+
return createUserError(ERROR_CODES.GMAIL_API_NOT_FOUND, ErrorCategory.GMAIL_API, "error", false, {
|
|
356
|
+
title: "Gmail API Not Found",
|
|
357
|
+
message: "The Gmail API endpoint could not be found. This may be a configuration issue.",
|
|
358
|
+
suggestions: [
|
|
359
|
+
"Verify the Gmail API is enabled in your Google Cloud project",
|
|
360
|
+
"Check that the API name is correct: 'gmail.api'",
|
|
361
|
+
"Try re-enabling the Gmail API in Google Cloud Console",
|
|
362
|
+
],
|
|
363
|
+
docsLink: "https://console.cloud.google.com/apis/library/gmail-api",
|
|
364
|
+
}, [
|
|
365
|
+
{
|
|
366
|
+
type: "config_change",
|
|
367
|
+
params: { setting: "gmail_api_enabled" },
|
|
368
|
+
description: "Enable Gmail API in Google Cloud Console",
|
|
369
|
+
},
|
|
370
|
+
], {});
|
|
178
371
|
}
|
|
179
372
|
// Rate limit
|
|
180
373
|
if (statusCode === 429) {
|
|
181
|
-
return createUserError(ErrorCategory.GMAIL_API, "
|
|
182
|
-
"
|
|
183
|
-
"
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
374
|
+
return createUserError(ERROR_CODES.GMAIL_RATE_LIMIT_EXCEEDED, ErrorCategory.GMAIL_API, "warning", true, {
|
|
375
|
+
title: "Gmail API Rate Limit Exceeded",
|
|
376
|
+
message: "Too many requests have been made to the Gmail API. You've hit the daily quota limit.",
|
|
377
|
+
suggestions: [
|
|
378
|
+
"Wait until tomorrow when the quota resets",
|
|
379
|
+
"Reduce sync frequency to avoid hitting the limit",
|
|
380
|
+
"Consider using Gmail push notifications instead of polling",
|
|
381
|
+
"Free tier: 250 quota units/day",
|
|
382
|
+
],
|
|
383
|
+
docsLink: "https://developers.google.com/gmail/api/v1/quota",
|
|
384
|
+
}, [
|
|
385
|
+
{
|
|
386
|
+
type: "wait",
|
|
387
|
+
description: "Wait until quota resets (usually daily)",
|
|
388
|
+
},
|
|
389
|
+
], { accountId });
|
|
187
390
|
}
|
|
188
391
|
// Generic Gmail error
|
|
189
|
-
return createUserError(ErrorCategory.GMAIL_API, "
|
|
190
|
-
"
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
392
|
+
return createUserError(ERROR_CODES.GMAIL_API_ERROR, ErrorCategory.GMAIL_API, "error", true, {
|
|
393
|
+
title: "Gmail API Error",
|
|
394
|
+
message: error.message ||
|
|
395
|
+
"An error occurred while communicating with Gmail",
|
|
396
|
+
suggestions: [
|
|
397
|
+
"Check your internet connection",
|
|
398
|
+
"Verify Gmail API is enabled in Google Cloud Console",
|
|
399
|
+
"Try again in a few minutes",
|
|
400
|
+
],
|
|
401
|
+
docsLink: "https://developers.google.com/gmail/api",
|
|
402
|
+
}, [
|
|
403
|
+
{
|
|
404
|
+
type: "retry",
|
|
405
|
+
delayMs: 60000,
|
|
406
|
+
description: "Retry after 1 minute",
|
|
407
|
+
},
|
|
408
|
+
], { accountId });
|
|
194
409
|
}
|
|
195
410
|
/**
|
|
196
411
|
* Parse network errors
|
|
@@ -200,36 +415,76 @@ export function parseNetworkError(error) {
|
|
|
200
415
|
// Connection refused
|
|
201
416
|
if (message.includes("econnrefused") ||
|
|
202
417
|
message.includes("connection refused")) {
|
|
203
|
-
return createUserError(ErrorCategory.NETWORK, "
|
|
204
|
-
"
|
|
205
|
-
"
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
418
|
+
return createUserError(ERROR_CODES.NETWORK_CONNECTION_REFUSED, ErrorCategory.NETWORK, "error", true, {
|
|
419
|
+
title: "Connection Refused",
|
|
420
|
+
message: "Could not connect to the server. The service may be down or your network is blocking the connection.",
|
|
421
|
+
suggestions: [
|
|
422
|
+
"Check your internet connection",
|
|
423
|
+
"Verify you're not behind a firewall or proxy",
|
|
424
|
+
"If using a VPN, try disconnecting it",
|
|
425
|
+
"Check if the service is temporarily down",
|
|
426
|
+
],
|
|
427
|
+
}, [
|
|
428
|
+
{
|
|
429
|
+
type: "retry",
|
|
430
|
+
delayMs: 30000,
|
|
431
|
+
description: "Retry after 30 seconds",
|
|
432
|
+
},
|
|
433
|
+
], {}, error);
|
|
209
434
|
}
|
|
210
435
|
// Timeout
|
|
211
436
|
if (message.includes("timeout") || message.includes("timed out")) {
|
|
212
|
-
return createUserError(ErrorCategory.NETWORK, "
|
|
213
|
-
"
|
|
214
|
-
"
|
|
215
|
-
|
|
216
|
-
|
|
437
|
+
return createUserError(ERROR_CODES.NETWORK_TIMEOUT, ErrorCategory.NETWORK, "warning", true, {
|
|
438
|
+
title: "Request Timeout",
|
|
439
|
+
message: "The request took too long to complete. This could be due to slow network or server issues.",
|
|
440
|
+
suggestions: [
|
|
441
|
+
"Check your internet connection speed",
|
|
442
|
+
"Try again in a few minutes",
|
|
443
|
+
"If syncing many transactions, consider reducing the date range",
|
|
444
|
+
],
|
|
445
|
+
}, [
|
|
446
|
+
{
|
|
447
|
+
type: "retry",
|
|
448
|
+
delayMs: 60000,
|
|
449
|
+
description: "Retry after 1 minute",
|
|
450
|
+
},
|
|
451
|
+
], {}, error);
|
|
217
452
|
}
|
|
218
453
|
// DNS resolution failed
|
|
219
454
|
if (message.includes("enotfound") || message.includes("getaddrinfo")) {
|
|
220
|
-
return createUserError(ErrorCategory.NETWORK, "
|
|
221
|
-
"
|
|
222
|
-
"
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
455
|
+
return createUserError(ERROR_CODES.NETWORK_DNS_FAILED, ErrorCategory.NETWORK, "error", false, {
|
|
456
|
+
title: "DNS Resolution Failed",
|
|
457
|
+
message: "Could not resolve the server address. This might be a DNS or network configuration issue.",
|
|
458
|
+
suggestions: [
|
|
459
|
+
"Check your internet connection",
|
|
460
|
+
"Try switching to a different DNS server (e.g., 8.8.8.8)",
|
|
461
|
+
"Flush your DNS cache",
|
|
462
|
+
"If you're using a VPN, try disconnecting it",
|
|
463
|
+
],
|
|
464
|
+
}, [
|
|
465
|
+
{
|
|
466
|
+
type: "manual_intervention",
|
|
467
|
+
description: "Manual network configuration may be required",
|
|
468
|
+
},
|
|
469
|
+
], {}, error);
|
|
226
470
|
}
|
|
227
471
|
// Generic network error
|
|
228
|
-
return createUserError(ErrorCategory.NETWORK, "
|
|
229
|
-
"
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
472
|
+
return createUserError(ERROR_CODES.NETWORK_GENERIC, ErrorCategory.NETWORK, "error", true, {
|
|
473
|
+
title: "Network Error",
|
|
474
|
+
message: error.message ||
|
|
475
|
+
"An error occurred while communicating with the server",
|
|
476
|
+
suggestions: [
|
|
477
|
+
"Check your internet connection",
|
|
478
|
+
"Try again in a few minutes",
|
|
479
|
+
"If the problem persists, check your network settings",
|
|
480
|
+
],
|
|
481
|
+
}, [
|
|
482
|
+
{
|
|
483
|
+
type: "retry",
|
|
484
|
+
delayMs: 60000,
|
|
485
|
+
description: "Retry after 1 minute",
|
|
486
|
+
},
|
|
487
|
+
], {}, error);
|
|
233
488
|
}
|
|
234
489
|
/**
|
|
235
490
|
* Parse file system errors
|
|
@@ -239,31 +494,69 @@ export function parseFileSystemError(error, filePath) {
|
|
|
239
494
|
const message = error.message;
|
|
240
495
|
// Permission denied
|
|
241
496
|
if (code === "EACCES" || code === "EPERM") {
|
|
242
|
-
return createUserError(ErrorCategory.FILE_SYSTEM, "
|
|
243
|
-
"
|
|
244
|
-
"
|
|
245
|
-
|
|
497
|
+
return createUserError(ERROR_CODES.FS_PERMISSION_DENIED, ErrorCategory.FILE_SYSTEM, "error", false, {
|
|
498
|
+
title: "Permission Denied",
|
|
499
|
+
message: `Cannot access ${filePath || "file or directory"}. You don't have the required permissions.`,
|
|
500
|
+
suggestions: [
|
|
501
|
+
"Check file/directory permissions",
|
|
502
|
+
"Ensure the user has read/write access to the data directory",
|
|
503
|
+
],
|
|
504
|
+
}, [
|
|
505
|
+
{
|
|
506
|
+
type: "manual_intervention",
|
|
507
|
+
description: "Fix file/directory permissions manually",
|
|
508
|
+
},
|
|
509
|
+
], { filePath }, error);
|
|
246
510
|
}
|
|
247
511
|
// No space left
|
|
248
512
|
if (code === "ENOSPC") {
|
|
249
|
-
return createUserError(ErrorCategory.STORAGE, "
|
|
250
|
-
"
|
|
251
|
-
"
|
|
252
|
-
|
|
513
|
+
return createUserError(ERROR_CODES.STORAGE_DISK_FULL, ErrorCategory.STORAGE, "fatal", false, {
|
|
514
|
+
title: "Disk Full",
|
|
515
|
+
message: "No space left on device. Cannot save transactions.",
|
|
516
|
+
suggestions: [
|
|
517
|
+
"Free up disk space by deleting unnecessary files",
|
|
518
|
+
"Consider moving the BillClaw data directory to a drive with more space",
|
|
519
|
+
],
|
|
520
|
+
}, [
|
|
521
|
+
{
|
|
522
|
+
type: "manual_intervention",
|
|
523
|
+
description: "Free up disk space manually",
|
|
524
|
+
},
|
|
525
|
+
], {}, error);
|
|
253
526
|
}
|
|
254
527
|
// Directory not found
|
|
255
528
|
if (code === "ENOENT" && message.includes("no such file")) {
|
|
256
|
-
return createUserError(ErrorCategory.FILE_SYSTEM, "
|
|
257
|
-
"
|
|
258
|
-
|
|
259
|
-
|
|
529
|
+
return createUserError(ERROR_CODES.FS_NOT_FOUND, ErrorCategory.FILE_SYSTEM, "error", false, {
|
|
530
|
+
title: "File or Directory Not Found",
|
|
531
|
+
message: `The file or directory ${filePath || ""} does not exist.`,
|
|
532
|
+
suggestions: [
|
|
533
|
+
"Run setup to initialize BillClaw",
|
|
534
|
+
"Verify the data directory path is correct",
|
|
535
|
+
],
|
|
536
|
+
}, [
|
|
537
|
+
{
|
|
538
|
+
type: "config_change",
|
|
539
|
+
params: { setting: "data_directory" },
|
|
540
|
+
description: "Update data directory path",
|
|
541
|
+
},
|
|
542
|
+
], { filePath }, error);
|
|
260
543
|
}
|
|
261
544
|
// Generic file system error
|
|
262
|
-
return createUserError(ErrorCategory.FILE_SYSTEM, "
|
|
263
|
-
"
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
545
|
+
return createUserError(ERROR_CODES.FS_GENERIC, ErrorCategory.FILE_SYSTEM, "error", true, {
|
|
546
|
+
title: "File System Error",
|
|
547
|
+
message: message ||
|
|
548
|
+
"An error occurred while accessing the file system",
|
|
549
|
+
suggestions: [
|
|
550
|
+
"Check file/directory permissions",
|
|
551
|
+
"Ensure the data directory exists and is writable",
|
|
552
|
+
"Try running setup to reinitialize",
|
|
553
|
+
],
|
|
554
|
+
}, [
|
|
555
|
+
{
|
|
556
|
+
type: "manual_intervention",
|
|
557
|
+
description: "Manual intervention may be required",
|
|
558
|
+
},
|
|
559
|
+
], { filePath }, error);
|
|
267
560
|
}
|
|
268
561
|
/**
|
|
269
562
|
* Type guard to check if error is a UserError
|
|
@@ -272,23 +565,29 @@ export function isUserError(error) {
|
|
|
272
565
|
return (typeof error === "object" &&
|
|
273
566
|
error !== null &&
|
|
274
567
|
"type" in error &&
|
|
275
|
-
error.type === "UserError"
|
|
568
|
+
error.type === "UserError" &&
|
|
569
|
+
"errorCode" in error &&
|
|
570
|
+
"humanReadable" in error);
|
|
276
571
|
}
|
|
277
572
|
/**
|
|
278
573
|
* Log error with context for debugging
|
|
279
574
|
*/
|
|
280
575
|
export function logError(logger, error, context) {
|
|
281
576
|
const logData = {
|
|
282
|
-
timestamp: new Date().toISOString(),
|
|
577
|
+
timestamp: isUserError(error) ? error.timestamp : new Date().toISOString(),
|
|
578
|
+
errorCode: isUserError(error) ? error.errorCode : undefined,
|
|
283
579
|
category: isUserError(error) ? error.category : ErrorCategory.UNKNOWN,
|
|
284
|
-
|
|
580
|
+
severity: isUserError(error) ? error.severity : undefined,
|
|
581
|
+
recoverable: isUserError(error) ? error.recoverable : undefined,
|
|
582
|
+
message: isUserError(error) ? error.humanReadable.message : error.message,
|
|
583
|
+
entities: isUserError(error) ? error.entities : undefined,
|
|
285
584
|
context,
|
|
286
585
|
};
|
|
287
|
-
if (isUserError(error) && error.
|
|
586
|
+
if (isUserError(error) && error.originalError) {
|
|
288
587
|
logData.originalError = {
|
|
289
|
-
name: error.
|
|
290
|
-
message: error.
|
|
291
|
-
stack: error.
|
|
588
|
+
name: error.originalError.name,
|
|
589
|
+
message: error.originalError.message,
|
|
590
|
+
stack: error.originalError.stack,
|
|
292
591
|
};
|
|
293
592
|
}
|
|
294
593
|
logger?.error?.("BillClaw error:", JSON.stringify(logData, null, 2));
|