@dizzlkheinz/ynab-mcpb 0.18.3 → 0.19.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/CHANGELOG.md +17 -0
- package/CLAUDE.md +87 -8
- package/bin/ynab-mcp-server.cjs +2 -2
- package/bin/ynab-mcp-server.js +3 -3
- package/biome.json +39 -0
- package/dist/bundle/index.cjs +67 -67
- package/dist/index.d.ts +1 -1
- package/dist/index.js +27 -27
- package/dist/server/YNABMCPServer.d.ts +3 -4
- package/dist/server/YNABMCPServer.js +111 -116
- package/dist/server/budgetResolver.d.ts +6 -5
- package/dist/server/budgetResolver.js +46 -36
- package/dist/server/cacheKeys.js +6 -6
- package/dist/server/cacheManager.js +14 -11
- package/dist/server/completions.d.ts +2 -2
- package/dist/server/completions.js +20 -15
- package/dist/server/config.d.ts +10 -5
- package/dist/server/config.js +24 -7
- package/dist/server/deltaCache.d.ts +2 -2
- package/dist/server/deltaCache.js +22 -16
- package/dist/server/deltaCache.merge.d.ts +2 -2
- package/dist/server/diagnostics.d.ts +4 -4
- package/dist/server/diagnostics.js +38 -32
- package/dist/server/errorHandler.d.ts +5 -12
- package/dist/server/errorHandler.js +219 -217
- package/dist/server/prompts.d.ts +2 -2
- package/dist/server/prompts.js +45 -45
- package/dist/server/rateLimiter.js +4 -4
- package/dist/server/requestLogger.d.ts +1 -1
- package/dist/server/requestLogger.js +40 -35
- package/dist/server/resources.d.ts +3 -3
- package/dist/server/resources.js +55 -52
- package/dist/server/responseFormatter.js +6 -6
- package/dist/server/securityMiddleware.d.ts +2 -2
- package/dist/server/securityMiddleware.js +22 -20
- package/dist/server/serverKnowledgeStore.js +1 -1
- package/dist/server/toolRegistry.d.ts +3 -3
- package/dist/server/toolRegistry.js +47 -40
- package/dist/tools/__tests__/deltaTestUtils.d.ts +3 -3
- package/dist/tools/__tests__/deltaTestUtils.js +2 -2
- package/dist/tools/accountTools.d.ts +9 -8
- package/dist/tools/accountTools.js +47 -47
- package/dist/tools/adapters.d.ts +13 -8
- package/dist/tools/adapters.js +21 -11
- package/dist/tools/budgetTools.d.ts +8 -7
- package/dist/tools/budgetTools.js +22 -22
- package/dist/tools/categoryTools.d.ts +9 -8
- package/dist/tools/categoryTools.js +68 -59
- package/dist/tools/compareTransactions/formatter.d.ts +3 -3
- package/dist/tools/compareTransactions/formatter.js +9 -9
- package/dist/tools/compareTransactions/index.d.ts +6 -6
- package/dist/tools/compareTransactions/index.js +58 -43
- package/dist/tools/compareTransactions/matcher.d.ts +1 -1
- package/dist/tools/compareTransactions/matcher.js +28 -15
- package/dist/tools/compareTransactions/parser.d.ts +2 -2
- package/dist/tools/compareTransactions/parser.js +144 -138
- package/dist/tools/compareTransactions/types.d.ts +4 -4
- package/dist/tools/compareTransactions.d.ts +1 -1
- package/dist/tools/compareTransactions.js +1 -1
- package/dist/tools/deltaFetcher.d.ts +2 -2
- package/dist/tools/deltaFetcher.js +16 -15
- package/dist/tools/deltaSupport.d.ts +4 -4
- package/dist/tools/deltaSupport.js +35 -41
- package/dist/tools/exportTransactions.d.ts +5 -4
- package/dist/tools/exportTransactions.js +61 -59
- package/dist/tools/monthTools.d.ts +7 -6
- package/dist/tools/monthTools.js +31 -29
- package/dist/tools/payeeTools.d.ts +7 -6
- package/dist/tools/payeeTools.js +28 -28
- package/dist/tools/reconcileAdapter.d.ts +2 -2
- package/dist/tools/reconcileAdapter.js +21 -11
- package/dist/tools/reconciliation/analyzer.d.ts +4 -4
- package/dist/tools/reconciliation/analyzer.js +136 -57
- package/dist/tools/reconciliation/csvParser.d.ts +3 -3
- package/dist/tools/reconciliation/csvParser.js +128 -104
- package/dist/tools/reconciliation/executor.d.ts +4 -4
- package/dist/tools/reconciliation/executor.js +148 -109
- package/dist/tools/reconciliation/index.d.ts +10 -10
- package/dist/tools/reconciliation/index.js +96 -83
- package/dist/tools/reconciliation/matcher.d.ts +3 -3
- package/dist/tools/reconciliation/matcher.js +17 -16
- package/dist/tools/reconciliation/payeeNormalizer.js +19 -8
- package/dist/tools/reconciliation/recommendationEngine.d.ts +1 -1
- package/dist/tools/reconciliation/recommendationEngine.js +40 -40
- package/dist/tools/reconciliation/reportFormatter.d.ts +2 -2
- package/dist/tools/reconciliation/reportFormatter.js +79 -54
- package/dist/tools/reconciliation/signDetector.d.ts +1 -1
- package/dist/tools/reconciliation/types.d.ts +19 -16
- package/dist/tools/reconciliation/ynabAdapter.d.ts +2 -2
- package/dist/tools/schemas/common.d.ts +1 -1
- package/dist/tools/schemas/common.js +1 -1
- package/dist/tools/schemas/outputs/accountOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/accountOutputs.js +24 -18
- package/dist/tools/schemas/outputs/budgetOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/budgetOutputs.js +14 -11
- package/dist/tools/schemas/outputs/categoryOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/categoryOutputs.js +49 -29
- package/dist/tools/schemas/outputs/comparisonOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/comparisonOutputs.js +12 -12
- package/dist/tools/schemas/outputs/index.d.ts +14 -14
- package/dist/tools/schemas/outputs/index.js +14 -14
- package/dist/tools/schemas/outputs/monthOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/monthOutputs.js +56 -41
- package/dist/tools/schemas/outputs/payeeOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/payeeOutputs.js +10 -10
- package/dist/tools/schemas/outputs/reconciliationOutputs.d.ts +2 -2
- package/dist/tools/schemas/outputs/reconciliationOutputs.js +45 -45
- package/dist/tools/schemas/outputs/transactionMutationOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/transactionMutationOutputs.js +28 -22
- package/dist/tools/schemas/outputs/transactionOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/transactionOutputs.js +43 -35
- package/dist/tools/schemas/outputs/utilityOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/utilityOutputs.js +5 -3
- package/dist/tools/schemas/shared/commonOutputs.d.ts +1 -1
- package/dist/tools/schemas/shared/commonOutputs.js +15 -9
- package/dist/tools/transactionReadTools.d.ts +11 -0
- package/dist/tools/transactionReadTools.js +202 -0
- package/dist/tools/transactionSchemas.d.ts +309 -0
- package/dist/tools/transactionSchemas.js +235 -0
- package/dist/tools/transactionTools.d.ts +6 -302
- package/dist/tools/transactionTools.js +7 -2054
- package/dist/tools/transactionUtils.d.ts +31 -0
- package/dist/tools/transactionUtils.js +364 -0
- package/dist/tools/transactionWriteTools.d.ts +20 -0
- package/dist/tools/transactionWriteTools.js +1342 -0
- package/dist/tools/utilityTools.d.ts +5 -4
- package/dist/tools/utilityTools.js +11 -11
- package/dist/types/index.d.ts +7 -7
- package/dist/types/index.js +6 -6
- package/dist/types/reconciliation.d.ts +1 -1
- package/dist/types/toolRegistration.d.ts +14 -12
- package/dist/utils/amountUtils.js +1 -1
- package/dist/utils/dateUtils.js +4 -4
- package/dist/utils/errors.d.ts +3 -3
- package/dist/utils/errors.js +4 -4
- package/dist/utils/money.d.ts +2 -2
- package/dist/utils/money.js +8 -8
- package/dist/utils/validationError.d.ts +1 -1
- package/dist/utils/validationError.js +1 -1
- package/docs/assets/examples/reconciliation-with-recommendations.json +66 -66
- package/docs/assets/schemas/reconciliation-v2.json +360 -336
- package/docs/plans/2025-12-25-transaction-tools-refactor-design.md +211 -0
- package/docs/plans/2025-12-25-transaction-tools-refactor.md +905 -0
- package/esbuild.config.mjs +53 -50
- package/meta.json +12548 -12548
- package/package.json +98 -109
- package/scripts/analyze-bundle.mjs +33 -30
- package/scripts/create-pr-description.js +169 -120
- package/scripts/run-all-tests.js +205 -0
- package/scripts/run-domain-integration-tests.js +28 -18
- package/scripts/run-generate-mcpb.js +19 -17
- package/scripts/run-throttled-integration-tests.js +92 -83
- package/scripts/test-delta-params.mjs +149 -120
- package/scripts/test-recommendations.ts +36 -32
- package/scripts/tmpTransaction.ts +80 -43
- package/scripts/validate-env.js +98 -91
- package/scripts/verify-build.js +78 -76
- package/src/__tests__/comprehensive.integration.test.ts +1281 -1154
- package/src/__tests__/performance.test.ts +723 -671
- package/src/__tests__/setup.ts +442 -395
- package/src/__tests__/smoke.e2e.test.ts +41 -39
- package/src/__tests__/testRunner.ts +314 -295
- package/src/__tests__/testUtils.ts +456 -364
- package/src/__tests__/tools/reconciliation/csvParser.integration.test.ts +109 -107
- package/src/__tests__/tools/reconciliation/real-world.integration.test.ts +41 -41
- package/src/index.ts +68 -59
- package/src/server/CLAUDE.md +480 -0
- package/src/server/YNABMCPServer.ts +821 -794
- package/src/server/__tests__/YNABMCPServer.integration.test.ts +929 -893
- package/src/server/__tests__/YNABMCPServer.test.ts +903 -899
- package/src/server/__tests__/budgetResolver.test.ts +466 -423
- package/src/server/__tests__/cacheManager.test.ts +891 -874
- package/src/server/__tests__/completions.integration.test.ts +115 -106
- package/src/server/__tests__/completions.test.ts +334 -313
- package/src/server/__tests__/config.test.ts +98 -86
- package/src/server/__tests__/deltaCache.merge.test.ts +774 -703
- package/src/server/__tests__/deltaCache.swr.test.ts +198 -153
- package/src/server/__tests__/deltaCache.test.ts +946 -759
- package/src/server/__tests__/diagnostics.test.ts +825 -792
- package/src/server/__tests__/errorHandler.integration.test.ts +512 -462
- package/src/server/__tests__/errorHandler.test.ts +402 -397
- package/src/server/__tests__/prompts.test.ts +424 -347
- package/src/server/__tests__/rateLimiter.test.ts +313 -309
- package/src/server/__tests__/requestLogger.test.ts +443 -403
- package/src/server/__tests__/resources.template.test.ts +196 -185
- package/src/server/__tests__/resources.test.ts +294 -288
- package/src/server/__tests__/security.integration.test.ts +487 -421
- package/src/server/__tests__/securityMiddleware.test.ts +519 -444
- package/src/server/__tests__/server-startup.integration.test.ts +509 -490
- package/src/server/__tests__/serverKnowledgeStore.test.ts +174 -173
- package/src/server/__tests__/toolRegistration.test.ts +239 -210
- package/src/server/__tests__/toolRegistry.test.ts +907 -845
- package/src/server/budgetResolver.ts +221 -181
- package/src/server/cacheKeys.ts +6 -6
- package/src/server/cacheManager.ts +498 -484
- package/src/server/completions.ts +267 -243
- package/src/server/config.ts +35 -14
- package/src/server/deltaCache.merge.ts +146 -128
- package/src/server/deltaCache.ts +352 -309
- package/src/server/diagnostics.ts +257 -242
- package/src/server/errorHandler.ts +747 -744
- package/src/server/prompts.ts +181 -176
- package/src/server/rateLimiter.ts +131 -129
- package/src/server/requestLogger.ts +350 -322
- package/src/server/resources.ts +442 -374
- package/src/server/responseFormatter.ts +41 -37
- package/src/server/securityMiddleware.ts +223 -205
- package/src/server/serverKnowledgeStore.ts +67 -67
- package/src/server/toolRegistry.ts +508 -474
- package/src/tools/CLAUDE.md +604 -0
- package/src/tools/__tests__/accountTools.delta.integration.test.ts +128 -111
- package/src/tools/__tests__/accountTools.integration.test.ts +129 -111
- package/src/tools/__tests__/accountTools.test.ts +685 -638
- package/src/tools/__tests__/adapters.test.ts +142 -108
- package/src/tools/__tests__/budgetTools.delta.integration.test.ts +73 -73
- package/src/tools/__tests__/budgetTools.integration.test.ts +132 -124
- package/src/tools/__tests__/budgetTools.test.ts +442 -413
- package/src/tools/__tests__/categoryTools.delta.integration.test.ts +76 -68
- package/src/tools/__tests__/categoryTools.integration.test.ts +314 -288
- package/src/tools/__tests__/categoryTools.test.ts +656 -625
- package/src/tools/__tests__/compareTransactions/formatter.test.ts +535 -462
- package/src/tools/__tests__/compareTransactions/index.test.ts +378 -358
- package/src/tools/__tests__/compareTransactions/matcher.test.ts +497 -398
- package/src/tools/__tests__/compareTransactions/parser.test.ts +765 -747
- package/src/tools/__tests__/compareTransactions.test.ts +352 -332
- package/src/tools/__tests__/compareTransactions.window.test.ts +150 -146
- package/src/tools/__tests__/deltaFetcher.scheduled.integration.test.ts +69 -65
- package/src/tools/__tests__/deltaFetcher.test.ts +325 -265
- package/src/tools/__tests__/deltaSupport.test.ts +211 -184
- package/src/tools/__tests__/deltaTestUtils.ts +37 -33
- package/src/tools/__tests__/exportTransactions.test.ts +205 -200
- package/src/tools/__tests__/monthTools.delta.integration.test.ts +68 -68
- package/src/tools/__tests__/monthTools.integration.test.ts +178 -166
- package/src/tools/__tests__/monthTools.test.ts +561 -512
- package/src/tools/__tests__/payeeTools.delta.integration.test.ts +68 -68
- package/src/tools/__tests__/payeeTools.integration.test.ts +158 -142
- package/src/tools/__tests__/payeeTools.test.ts +486 -434
- package/src/tools/__tests__/transactionSchemas.test.ts +1204 -0
- package/src/tools/__tests__/transactionTools.integration.test.ts +875 -825
- package/src/tools/__tests__/transactionTools.test.ts +4923 -4366
- package/src/tools/__tests__/transactionUtils.test.ts +1016 -0
- package/src/tools/__tests__/utilityTools.integration.test.ts +32 -32
- package/src/tools/__tests__/utilityTools.test.ts +68 -58
- package/src/tools/accountTools.ts +293 -271
- package/src/tools/adapters.ts +120 -63
- package/src/tools/budgetTools.ts +121 -116
- package/src/tools/categoryTools.ts +379 -339
- package/src/tools/compareTransactions/formatter.ts +131 -119
- package/src/tools/compareTransactions/index.ts +249 -214
- package/src/tools/compareTransactions/matcher.ts +259 -209
- package/src/tools/compareTransactions/parser.ts +517 -487
- package/src/tools/compareTransactions/types.ts +38 -38
- package/src/tools/compareTransactions.ts +1 -1
- package/src/tools/deltaFetcher.ts +281 -260
- package/src/tools/deltaSupport.ts +264 -259
- package/src/tools/exportTransactions.ts +230 -218
- package/src/tools/monthTools.ts +180 -165
- package/src/tools/payeeTools.ts +152 -140
- package/src/tools/reconcileAdapter.ts +297 -246
- package/src/tools/reconciliation/CLAUDE.md +506 -0
- package/src/tools/reconciliation/__tests__/adapter.causes.test.ts +135 -112
- package/src/tools/reconciliation/__tests__/adapter.test.ts +249 -227
- package/src/tools/reconciliation/__tests__/analyzer.test.ts +408 -335
- package/src/tools/reconciliation/__tests__/csvParser.test.ts +71 -69
- package/src/tools/reconciliation/__tests__/executor.integration.test.ts +348 -323
- package/src/tools/reconciliation/__tests__/executor.progress.test.ts +503 -457
- package/src/tools/reconciliation/__tests__/executor.test.ts +898 -831
- package/src/tools/reconciliation/__tests__/matcher.test.ts +667 -663
- package/src/tools/reconciliation/__tests__/payeeNormalizer.test.ts +296 -276
- package/src/tools/reconciliation/__tests__/recommendationEngine.integration.test.ts +692 -624
- package/src/tools/reconciliation/__tests__/recommendationEngine.test.ts +1008 -986
- package/src/tools/reconciliation/__tests__/reconciliation.delta.integration.test.ts +187 -146
- package/src/tools/reconciliation/__tests__/reportFormatter.test.ts +583 -530
- package/src/tools/reconciliation/__tests__/scenarios/adapterCurrency.scenario.test.ts +75 -71
- package/src/tools/reconciliation/__tests__/scenarios/extremes.scenario.test.ts +70 -58
- package/src/tools/reconciliation/__tests__/scenarios/repeatAmount.scenario.test.ts +102 -88
- package/src/tools/reconciliation/__tests__/schemaUrl.test.ts +58 -43
- package/src/tools/reconciliation/__tests__/signDetector.test.ts +209 -206
- package/src/tools/reconciliation/__tests__/ynabAdapter.test.ts +66 -60
- package/src/tools/reconciliation/analyzer.ts +582 -406
- package/src/tools/reconciliation/csvParser.ts +656 -609
- package/src/tools/reconciliation/executor.ts +1290 -1128
- package/src/tools/reconciliation/index.ts +580 -528
- package/src/tools/reconciliation/matcher.ts +256 -240
- package/src/tools/reconciliation/payeeNormalizer.ts +92 -78
- package/src/tools/reconciliation/recommendationEngine.ts +357 -345
- package/src/tools/reconciliation/reportFormatter.ts +349 -276
- package/src/tools/reconciliation/signDetector.ts +89 -83
- package/src/tools/reconciliation/types.ts +164 -153
- package/src/tools/reconciliation/ynabAdapter.ts +17 -15
- package/src/tools/schemas/CLAUDE.md +546 -0
- package/src/tools/schemas/common.ts +1 -1
- package/src/tools/schemas/outputs/__tests__/accountOutputs.test.ts +410 -409
- package/src/tools/schemas/outputs/__tests__/budgetOutputs.test.ts +305 -299
- package/src/tools/schemas/outputs/__tests__/categoryOutputs.test.ts +431 -430
- package/src/tools/schemas/outputs/__tests__/comparisonOutputs.test.ts +510 -495
- package/src/tools/schemas/outputs/__tests__/dateValidation.test.ts +179 -153
- package/src/tools/schemas/outputs/__tests__/discrepancyDirection.test.ts +293 -254
- package/src/tools/schemas/outputs/__tests__/monthOutputs.test.ts +457 -457
- package/src/tools/schemas/outputs/__tests__/payeeOutputs.test.ts +362 -356
- package/src/tools/schemas/outputs/__tests__/reconciliationOutputs.test.ts +402 -399
- package/src/tools/schemas/outputs/__tests__/transactionMutationSchemas.test.ts +225 -211
- package/src/tools/schemas/outputs/__tests__/transactionOutputs.test.ts +457 -454
- package/src/tools/schemas/outputs/__tests__/utilityOutputs.test.ts +316 -315
- package/src/tools/schemas/outputs/accountOutputs.ts +40 -34
- package/src/tools/schemas/outputs/budgetOutputs.ts +24 -19
- package/src/tools/schemas/outputs/categoryOutputs.ts +76 -56
- package/src/tools/schemas/outputs/comparisonOutputs.ts +192 -169
- package/src/tools/schemas/outputs/index.ts +163 -163
- package/src/tools/schemas/outputs/monthOutputs.ts +95 -80
- package/src/tools/schemas/outputs/payeeOutputs.ts +18 -18
- package/src/tools/schemas/outputs/reconciliationOutputs.ts +386 -373
- package/src/tools/schemas/outputs/transactionMutationOutputs.ts +259 -231
- package/src/tools/schemas/outputs/transactionOutputs.ts +81 -71
- package/src/tools/schemas/outputs/utilityOutputs.ts +90 -84
- package/src/tools/schemas/shared/commonOutputs.ts +27 -19
- package/src/tools/toolCategories.ts +114 -114
- package/src/tools/transactionReadTools.ts +327 -0
- package/src/tools/transactionSchemas.ts +484 -0
- package/src/tools/transactionTools.ts +107 -2990
- package/src/tools/transactionUtils.ts +621 -0
- package/src/tools/transactionWriteTools.ts +2110 -0
- package/src/tools/utilityTools.ts +46 -41
- package/src/types/CLAUDE.md +477 -0
- package/src/types/__tests__/index.test.ts +51 -51
- package/src/types/index.ts +43 -39
- package/src/types/integration-tests.d.ts +26 -26
- package/src/types/reconciliation.ts +29 -29
- package/src/types/toolAnnotations.ts +30 -30
- package/src/types/toolRegistration.ts +43 -32
- package/src/utils/CLAUDE.md +508 -0
- package/src/utils/__tests__/dateUtils.test.ts +174 -168
- package/src/utils/__tests__/money.test.ts +193 -187
- package/src/utils/amountUtils.ts +5 -5
- package/src/utils/baseError.ts +5 -5
- package/src/utils/dateUtils.ts +29 -26
- package/src/utils/errors.ts +14 -14
- package/src/utils/money.ts +66 -52
- package/src/utils/validationError.ts +1 -1
- package/tsconfig.json +29 -29
- package/tsconfig.prod.json +16 -16
- package/vitest-reporters/split-json-reporter.ts +247 -204
- package/vitest.config.ts +99 -95
- package/.prettierignore +0 -10
- package/.prettierrc.json +0 -10
- package/eslint.config.js +0 -49
|
@@ -1,774 +1,760 @@
|
|
|
1
|
-
import { CallToolResult } from
|
|
1
|
+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Response formatter contract for dependency injection in error handling
|
|
5
5
|
*/
|
|
6
6
|
interface ErrorResponseFormatter {
|
|
7
|
-
|
|
7
|
+
format(value: unknown): string;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* YNAB API error codes and their corresponding HTTP status codes
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
export
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
export enum YNABErrorCode {
|
|
15
|
+
BAD_REQUEST = 400,
|
|
16
|
+
UNAUTHORIZED = 401,
|
|
17
|
+
FORBIDDEN = 403,
|
|
18
|
+
NOT_FOUND = 404,
|
|
19
|
+
TOO_MANY_REQUESTS = 429,
|
|
20
|
+
INTERNAL_SERVER_ERROR = 500,
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Security-related error codes
|
|
25
25
|
*/
|
|
26
|
-
export
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
export enum SecurityErrorCode {
|
|
27
|
+
RATE_LIMIT_EXCEEDED = "RATE_LIMIT_EXCEEDED",
|
|
28
|
+
VALIDATION_ERROR = "VALIDATION_ERROR",
|
|
29
|
+
UNKNOWN_ERROR = "UNKNOWN_ERROR",
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* Standardized error response structure
|
|
34
34
|
*/
|
|
35
35
|
export interface ErrorResponse {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
36
|
+
error: {
|
|
37
|
+
code: YNABErrorCode | SecurityErrorCode;
|
|
38
|
+
message: string;
|
|
39
|
+
userMessage: string; // User-friendly message
|
|
40
|
+
details?: string | Record<string, unknown>;
|
|
41
|
+
suggestions?: string[]; // Actionable suggestions for the user
|
|
42
|
+
};
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
46
|
* Custom error classes for different error types
|
|
47
47
|
*/
|
|
48
48
|
export class YNABAPIError extends Error {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
49
|
+
public readonly code: YNABErrorCode;
|
|
50
|
+
public readonly originalError?: unknown;
|
|
51
|
+
|
|
52
|
+
constructor(code: YNABErrorCode, message: string, originalError?: unknown) {
|
|
53
|
+
super(message);
|
|
54
|
+
this.name = "YNABAPIError";
|
|
55
|
+
this.code = code;
|
|
56
|
+
this.originalError = originalError;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Expose status as an alias for code for backward compatibility with tests
|
|
60
|
+
get status(): YNABErrorCode {
|
|
61
|
+
return this.code;
|
|
62
|
+
}
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
export class ValidationError extends Error {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
66
|
+
public readonly details?: string | undefined;
|
|
67
|
+
public readonly suggestions?: string[] | undefined;
|
|
68
|
+
|
|
69
|
+
constructor(
|
|
70
|
+
message: string,
|
|
71
|
+
details?: string | undefined,
|
|
72
|
+
suggestions?: string[] | undefined,
|
|
73
|
+
) {
|
|
74
|
+
super(message);
|
|
75
|
+
this.name = "ValidationError";
|
|
76
|
+
this.details = details;
|
|
77
|
+
this.suggestions = suggestions;
|
|
78
|
+
}
|
|
75
79
|
}
|
|
76
80
|
|
|
77
81
|
/**
|
|
78
82
|
* Centralized error handling middleware for all YNAB MCP tools
|
|
79
83
|
*/
|
|
80
84
|
export class ErrorHandler {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
createYNABError(code: YNABErrorCode, context: string, originalError?: unknown): YNABAPIError {
|
|
755
|
-
const message = this.getErrorMessage(code, context);
|
|
756
|
-
return new YNABAPIError(code, message, originalError);
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
/**
|
|
760
|
-
* Static method for backward compatibility
|
|
761
|
-
*/
|
|
762
|
-
static createYNABError(
|
|
763
|
-
code: YNABErrorCode,
|
|
764
|
-
context: string,
|
|
765
|
-
originalError?: unknown,
|
|
766
|
-
): YNABAPIError {
|
|
767
|
-
if (!ErrorHandler.defaultInstance) {
|
|
768
|
-
ErrorHandler.defaultInstance = new ErrorHandler(ErrorHandler.createFallbackFormatter());
|
|
769
|
-
}
|
|
770
|
-
return ErrorHandler.defaultInstance.createYNABError(code, context, originalError);
|
|
771
|
-
}
|
|
85
|
+
private formatter: ErrorResponseFormatter;
|
|
86
|
+
|
|
87
|
+
constructor(formatter: ErrorResponseFormatter) {
|
|
88
|
+
this.formatter = formatter;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Handles errors from YNAB API calls and returns standardized MCP responses
|
|
93
|
+
*/
|
|
94
|
+
handleError(error: unknown, context: string): CallToolResult {
|
|
95
|
+
const errorResponse = this.createErrorResponse(error, context);
|
|
96
|
+
|
|
97
|
+
let formattedText: string;
|
|
98
|
+
try {
|
|
99
|
+
formattedText = this.formatter.format(errorResponse);
|
|
100
|
+
} catch {
|
|
101
|
+
// Fallback to JSON.stringify if formatter fails
|
|
102
|
+
try {
|
|
103
|
+
formattedText = JSON.stringify(errorResponse, null, 2);
|
|
104
|
+
} catch {
|
|
105
|
+
// Final fallback if JSON serialization fails (e.g. circular references)
|
|
106
|
+
formattedText = `Error processing request: ${this.getGenericErrorMessage(context)}`;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
isError: true,
|
|
112
|
+
content: [
|
|
113
|
+
{
|
|
114
|
+
type: "text",
|
|
115
|
+
text: formattedText,
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Creates a standardized error response based on the error type
|
|
123
|
+
*/
|
|
124
|
+
private createErrorResponse(error: unknown, context: string): ErrorResponse {
|
|
125
|
+
// Handle custom error types
|
|
126
|
+
if (error instanceof YNABAPIError) {
|
|
127
|
+
const ynabDetails = this.extractYNABApiError(error.originalError);
|
|
128
|
+
const detailsToSanitize = ynabDetails?.details || error.originalError;
|
|
129
|
+
const sanitizedDetails = this.sanitizeErrorDetails(detailsToSanitize);
|
|
130
|
+
return {
|
|
131
|
+
error: {
|
|
132
|
+
code: error.code,
|
|
133
|
+
message: this.getErrorMessage(error.code, context),
|
|
134
|
+
userMessage: this.getUserFriendlyMessage(error.code, context),
|
|
135
|
+
suggestions: this.getErrorSuggestions(error.code, context),
|
|
136
|
+
...(sanitizedDetails && { details: sanitizedDetails }),
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (error instanceof ValidationError) {
|
|
142
|
+
const sanitizedDetails = error.details
|
|
143
|
+
? this.sanitizeErrorDetails(error.details)
|
|
144
|
+
: undefined;
|
|
145
|
+
const suggestions =
|
|
146
|
+
error.suggestions && error.suggestions.length > 0
|
|
147
|
+
? error.suggestions
|
|
148
|
+
: this.getErrorSuggestions(
|
|
149
|
+
SecurityErrorCode.VALIDATION_ERROR,
|
|
150
|
+
context,
|
|
151
|
+
);
|
|
152
|
+
return {
|
|
153
|
+
error: {
|
|
154
|
+
code: SecurityErrorCode.VALIDATION_ERROR,
|
|
155
|
+
message: error.message,
|
|
156
|
+
userMessage: this.getUserFriendlyMessage(
|
|
157
|
+
SecurityErrorCode.VALIDATION_ERROR,
|
|
158
|
+
context,
|
|
159
|
+
),
|
|
160
|
+
suggestions,
|
|
161
|
+
...(sanitizedDetails && { details: sanitizedDetails }),
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const ynabApiError = this.extractYNABApiError(error);
|
|
167
|
+
if (ynabApiError) {
|
|
168
|
+
const sanitizedDetails = ynabApiError.details
|
|
169
|
+
? this.sanitizeErrorDetails(ynabApiError.details)
|
|
170
|
+
: undefined;
|
|
171
|
+
return {
|
|
172
|
+
error: {
|
|
173
|
+
code: ynabApiError.code,
|
|
174
|
+
message: this.getErrorMessage(ynabApiError.code, context),
|
|
175
|
+
userMessage: this.getUserFriendlyMessage(ynabApiError.code, context),
|
|
176
|
+
suggestions: this.getErrorSuggestions(ynabApiError.code, context),
|
|
177
|
+
...(sanitizedDetails && { details: sanitizedDetails }),
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Handle generic errors by analyzing the error message
|
|
183
|
+
|
|
184
|
+
const httpStatus = this.extractHttpStatus(error);
|
|
185
|
+
if (httpStatus !== null) {
|
|
186
|
+
const code = this.mapHttpStatusToErrorCode(httpStatus);
|
|
187
|
+
if (code) {
|
|
188
|
+
const details = this.extractHttpStatusDetails(error);
|
|
189
|
+
return {
|
|
190
|
+
error: {
|
|
191
|
+
code,
|
|
192
|
+
message: this.getErrorMessage(code, context),
|
|
193
|
+
userMessage: this.getUserFriendlyMessage(code, context),
|
|
194
|
+
suggestions: this.getErrorSuggestions(code, context),
|
|
195
|
+
...(details && { details }),
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Handle generic errors by analyzing the error message
|
|
202
|
+
if (error instanceof Error) {
|
|
203
|
+
const detectedCode = this.detectErrorCode(error);
|
|
204
|
+
if (detectedCode) {
|
|
205
|
+
return {
|
|
206
|
+
error: {
|
|
207
|
+
code: detectedCode,
|
|
208
|
+
message: this.getErrorMessage(detectedCode, context),
|
|
209
|
+
userMessage: this.getUserFriendlyMessage(detectedCode, context),
|
|
210
|
+
suggestions: this.getErrorSuggestions(detectedCode, context),
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Fallback for unknown errors
|
|
217
|
+
// Preserve the original error message for debugging while sanitizing sensitive data
|
|
218
|
+
let errorMessage: string;
|
|
219
|
+
if (error instanceof Error) {
|
|
220
|
+
errorMessage = error.message;
|
|
221
|
+
} else if (typeof error === "string") {
|
|
222
|
+
errorMessage = error;
|
|
223
|
+
} else if (error && typeof error === "object") {
|
|
224
|
+
// Handle plain objects (e.g., YNAB SDK errors that aren't Error instances)
|
|
225
|
+
try {
|
|
226
|
+
errorMessage = JSON.stringify(error, null, 2);
|
|
227
|
+
} catch {
|
|
228
|
+
// Circular reference or other JSON issue
|
|
229
|
+
errorMessage = Object.prototype.toString.call(error);
|
|
230
|
+
}
|
|
231
|
+
} else {
|
|
232
|
+
errorMessage = String(error);
|
|
233
|
+
}
|
|
234
|
+
const sanitizedDetails = this.sanitizeErrorDetails(errorMessage);
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
error: {
|
|
238
|
+
code: SecurityErrorCode.UNKNOWN_ERROR,
|
|
239
|
+
message: this.getGenericErrorMessage(context),
|
|
240
|
+
userMessage: this.getUserFriendlyGenericMessage(context),
|
|
241
|
+
suggestions: [
|
|
242
|
+
"Try the operation again",
|
|
243
|
+
"Check your internet connection",
|
|
244
|
+
"Contact support if the issue persists",
|
|
245
|
+
],
|
|
246
|
+
...(sanitizedDetails && { details: sanitizedDetails }),
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Detects YNAB error codes from error messages
|
|
253
|
+
*/
|
|
254
|
+
private detectErrorCode(error: Error): YNABErrorCode | null {
|
|
255
|
+
const message = error.message.toLowerCase();
|
|
256
|
+
|
|
257
|
+
if (message.includes("401") || message.includes("unauthorized")) {
|
|
258
|
+
return YNABErrorCode.UNAUTHORIZED;
|
|
259
|
+
}
|
|
260
|
+
if (message.includes("403") || message.includes("forbidden")) {
|
|
261
|
+
return YNABErrorCode.FORBIDDEN;
|
|
262
|
+
}
|
|
263
|
+
if (message.includes("404") || message.includes("not found")) {
|
|
264
|
+
return YNABErrorCode.NOT_FOUND;
|
|
265
|
+
}
|
|
266
|
+
if (message.includes("429") || message.includes("too many requests")) {
|
|
267
|
+
return YNABErrorCode.TOO_MANY_REQUESTS;
|
|
268
|
+
}
|
|
269
|
+
if (message.includes("500") || message.includes("internal server error")) {
|
|
270
|
+
return YNABErrorCode.INTERNAL_SERVER_ERROR;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Returns user-friendly error messages for end users
|
|
278
|
+
*/
|
|
279
|
+
private getUserFriendlyMessage(
|
|
280
|
+
code: YNABErrorCode | SecurityErrorCode,
|
|
281
|
+
context: string,
|
|
282
|
+
): string {
|
|
283
|
+
switch (code) {
|
|
284
|
+
case YNABErrorCode.BAD_REQUEST:
|
|
285
|
+
return "The request was invalid. Please check your input data.";
|
|
286
|
+
case YNABErrorCode.UNAUTHORIZED:
|
|
287
|
+
return "Your YNAB access token is invalid or has expired. Please check your token and try again.";
|
|
288
|
+
case YNABErrorCode.FORBIDDEN:
|
|
289
|
+
return "You don't have permission to access this YNAB data. Please check your account permissions.";
|
|
290
|
+
case YNABErrorCode.NOT_FOUND:
|
|
291
|
+
return this.getUserFriendlyNotFoundMessage(context);
|
|
292
|
+
case YNABErrorCode.TOO_MANY_REQUESTS:
|
|
293
|
+
return "We're making too many requests to YNAB. Please wait a moment and try again.";
|
|
294
|
+
case YNABErrorCode.INTERNAL_SERVER_ERROR:
|
|
295
|
+
return "YNAB's servers are having issues. Please try again in a few minutes.";
|
|
296
|
+
case SecurityErrorCode.VALIDATION_ERROR:
|
|
297
|
+
return "Some of the information provided is invalid. Please check your inputs and try again.";
|
|
298
|
+
case SecurityErrorCode.RATE_LIMIT_EXCEEDED:
|
|
299
|
+
return "Too many requests have been made. Please wait before trying again.";
|
|
300
|
+
default:
|
|
301
|
+
return this.getUserFriendlyGenericMessage(context);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Returns actionable suggestions for users based on error type
|
|
307
|
+
*/
|
|
308
|
+
private getErrorSuggestions(
|
|
309
|
+
code: YNABErrorCode | SecurityErrorCode,
|
|
310
|
+
context: string,
|
|
311
|
+
): string[] {
|
|
312
|
+
switch (code) {
|
|
313
|
+
case YNABErrorCode.BAD_REQUEST:
|
|
314
|
+
return [
|
|
315
|
+
"Check that all required fields are correct",
|
|
316
|
+
"Verify that dates are in the correct format (ISO 8601)",
|
|
317
|
+
"Ensure amounts are valid numbers",
|
|
318
|
+
];
|
|
319
|
+
case YNABErrorCode.UNAUTHORIZED:
|
|
320
|
+
return [
|
|
321
|
+
"Go to https://app.youneedabudget.com/settings/developer to generate a new access token",
|
|
322
|
+
"Make sure you copied the entire token without any extra spaces",
|
|
323
|
+
"Check that your token hasn't expired",
|
|
324
|
+
];
|
|
325
|
+
case YNABErrorCode.FORBIDDEN:
|
|
326
|
+
return [
|
|
327
|
+
"Verify that your YNAB account has access to the requested budget",
|
|
328
|
+
"Check if your YNAB subscription is active",
|
|
329
|
+
"Try logging into YNAB directly to confirm access",
|
|
330
|
+
];
|
|
331
|
+
case YNABErrorCode.NOT_FOUND:
|
|
332
|
+
return this.getNotFoundSuggestions(context);
|
|
333
|
+
case YNABErrorCode.TOO_MANY_REQUESTS:
|
|
334
|
+
return [
|
|
335
|
+
"Wait 1-2 minutes before trying again",
|
|
336
|
+
"Try making fewer requests at once",
|
|
337
|
+
"The system will automatically retry after a short delay",
|
|
338
|
+
];
|
|
339
|
+
case YNABErrorCode.INTERNAL_SERVER_ERROR:
|
|
340
|
+
return [
|
|
341
|
+
"Check YNAB's status page at https://status.youneedabudget.com",
|
|
342
|
+
"Try again in a few minutes",
|
|
343
|
+
"Contact YNAB support if the issue persists",
|
|
344
|
+
];
|
|
345
|
+
case SecurityErrorCode.VALIDATION_ERROR:
|
|
346
|
+
return [
|
|
347
|
+
"Double-check all required fields are filled out",
|
|
348
|
+
"Verify that amounts are in the correct format",
|
|
349
|
+
"Make sure dates are valid and in the right format",
|
|
350
|
+
];
|
|
351
|
+
default:
|
|
352
|
+
return [
|
|
353
|
+
"Try the operation again",
|
|
354
|
+
"Check your internet connection",
|
|
355
|
+
"Contact support if the issue persists",
|
|
356
|
+
];
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Returns user-friendly not found messages
|
|
362
|
+
*/
|
|
363
|
+
private getUserFriendlyNotFoundMessage(context: string): string {
|
|
364
|
+
if (context.includes("account")) {
|
|
365
|
+
return "We couldn't find the budget or account you're looking for.";
|
|
366
|
+
}
|
|
367
|
+
if (context.includes("budget")) {
|
|
368
|
+
return "We couldn't find that budget. It may have been deleted or you may not have access.";
|
|
369
|
+
}
|
|
370
|
+
if (context.includes("category")) {
|
|
371
|
+
return "We couldn't find that category. It may have been deleted or moved.";
|
|
372
|
+
}
|
|
373
|
+
if (context.includes("transaction")) {
|
|
374
|
+
return "We couldn't find that transaction. It may have been deleted or moved.";
|
|
375
|
+
}
|
|
376
|
+
if (context.includes("payee")) {
|
|
377
|
+
return "We couldn't find that payee in your budget.";
|
|
378
|
+
}
|
|
379
|
+
return "We couldn't find what you're looking for. Please check that all information is correct.";
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Returns suggestions for not found errors
|
|
384
|
+
*/
|
|
385
|
+
private getNotFoundSuggestions(context: string): string[] {
|
|
386
|
+
const baseSuggestions = [
|
|
387
|
+
"Double-check that the name or ID is spelled correctly",
|
|
388
|
+
"Try refreshing your budget data",
|
|
389
|
+
"Make sure you're using the right budget",
|
|
390
|
+
];
|
|
391
|
+
|
|
392
|
+
if (context.includes("account")) {
|
|
393
|
+
return [
|
|
394
|
+
...baseSuggestions,
|
|
395
|
+
"Check if the account was recently closed or renamed",
|
|
396
|
+
];
|
|
397
|
+
}
|
|
398
|
+
if (context.includes("category")) {
|
|
399
|
+
return [
|
|
400
|
+
...baseSuggestions,
|
|
401
|
+
"Check if the category was deleted or moved to a different group",
|
|
402
|
+
];
|
|
403
|
+
}
|
|
404
|
+
if (context.includes("transaction")) {
|
|
405
|
+
return [
|
|
406
|
+
...baseSuggestions,
|
|
407
|
+
"Check if the transaction was deleted or is in a different account",
|
|
408
|
+
];
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return baseSuggestions;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Returns user-friendly generic error message
|
|
416
|
+
*/
|
|
417
|
+
private getUserFriendlyGenericMessage(context: string): string {
|
|
418
|
+
if (context.includes("transaction")) {
|
|
419
|
+
return "There was a problem with your transaction. Please check your information and try again.";
|
|
420
|
+
}
|
|
421
|
+
if (context.includes("budget")) {
|
|
422
|
+
return "There was a problem accessing your budget data. Please try again.";
|
|
423
|
+
}
|
|
424
|
+
if (context.includes("account")) {
|
|
425
|
+
return "There was a problem accessing your account information. Please try again.";
|
|
426
|
+
}
|
|
427
|
+
return "Something went wrong. Please try again in a moment.";
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Returns user-friendly error messages for different error codes
|
|
432
|
+
*/
|
|
433
|
+
private getErrorMessage(code: YNABErrorCode, context: string): string {
|
|
434
|
+
switch (code) {
|
|
435
|
+
case YNABErrorCode.BAD_REQUEST:
|
|
436
|
+
return "Bad request - invalid parameters";
|
|
437
|
+
case YNABErrorCode.UNAUTHORIZED:
|
|
438
|
+
return "Invalid or expired YNAB access token";
|
|
439
|
+
case YNABErrorCode.FORBIDDEN:
|
|
440
|
+
return "Insufficient permissions to access YNAB data";
|
|
441
|
+
case YNABErrorCode.NOT_FOUND:
|
|
442
|
+
return this.getNotFoundMessage(context);
|
|
443
|
+
case YNABErrorCode.TOO_MANY_REQUESTS:
|
|
444
|
+
return "Rate limit exceeded. Please try again later";
|
|
445
|
+
case YNABErrorCode.INTERNAL_SERVER_ERROR:
|
|
446
|
+
return "YNAB service is currently unavailable";
|
|
447
|
+
default:
|
|
448
|
+
return this.getGenericErrorMessage(context);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Returns context-specific not found error messages
|
|
454
|
+
*/
|
|
455
|
+
private getNotFoundMessage(context: string): string {
|
|
456
|
+
if (context.includes("listing accounts")) {
|
|
457
|
+
return "Failed to list accounts - budget or account not found";
|
|
458
|
+
}
|
|
459
|
+
if (context.includes("getting account")) {
|
|
460
|
+
return "Failed to get account - budget or account not found";
|
|
461
|
+
}
|
|
462
|
+
if (
|
|
463
|
+
context.includes("listing budgets") ||
|
|
464
|
+
context.includes("getting budget")
|
|
465
|
+
) {
|
|
466
|
+
return "Budget not found";
|
|
467
|
+
}
|
|
468
|
+
if (
|
|
469
|
+
context.includes("listing categories") ||
|
|
470
|
+
context.includes("getting category")
|
|
471
|
+
) {
|
|
472
|
+
return "Budget or category not found";
|
|
473
|
+
}
|
|
474
|
+
if (
|
|
475
|
+
context.includes("listing months") ||
|
|
476
|
+
context.includes("getting month")
|
|
477
|
+
) {
|
|
478
|
+
return "Budget or month not found";
|
|
479
|
+
}
|
|
480
|
+
if (
|
|
481
|
+
context.includes("listing payees") ||
|
|
482
|
+
context.includes("getting payee")
|
|
483
|
+
) {
|
|
484
|
+
return "Budget or payee not found";
|
|
485
|
+
}
|
|
486
|
+
if (
|
|
487
|
+
context.includes("listing transactions") ||
|
|
488
|
+
context.includes("getting transaction")
|
|
489
|
+
) {
|
|
490
|
+
return "Budget, account, category, or transaction not found";
|
|
491
|
+
}
|
|
492
|
+
return "The requested resource was not found. Please verify the provided IDs are correct.";
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Returns context-specific generic error messages
|
|
497
|
+
*/
|
|
498
|
+
private getGenericErrorMessage(context: string): string {
|
|
499
|
+
if (context.includes("listing accounts")) {
|
|
500
|
+
return "Failed to list accounts";
|
|
501
|
+
}
|
|
502
|
+
if (context.includes("getting account")) {
|
|
503
|
+
return "Failed to get account";
|
|
504
|
+
}
|
|
505
|
+
if (context.includes("creating account")) {
|
|
506
|
+
return "Failed to create account";
|
|
507
|
+
}
|
|
508
|
+
if (context.includes("listing budgets")) {
|
|
509
|
+
return "Failed to list budgets";
|
|
510
|
+
}
|
|
511
|
+
if (context.includes("getting budget")) {
|
|
512
|
+
return "Failed to get budget";
|
|
513
|
+
}
|
|
514
|
+
if (context.includes("listing categories")) {
|
|
515
|
+
return "Failed to list categories";
|
|
516
|
+
}
|
|
517
|
+
if (context.includes("getting category")) {
|
|
518
|
+
return "Failed to get category";
|
|
519
|
+
}
|
|
520
|
+
if (context.includes("updating category")) {
|
|
521
|
+
return "Failed to update category";
|
|
522
|
+
}
|
|
523
|
+
if (context.includes("listing months")) {
|
|
524
|
+
return "Failed to list months";
|
|
525
|
+
}
|
|
526
|
+
if (context.includes("getting month")) {
|
|
527
|
+
return "Failed to get month data";
|
|
528
|
+
}
|
|
529
|
+
if (context.includes("listing payees")) {
|
|
530
|
+
return "Failed to list payees";
|
|
531
|
+
}
|
|
532
|
+
if (context.includes("getting payee")) {
|
|
533
|
+
return "Failed to get payee";
|
|
534
|
+
}
|
|
535
|
+
if (context.includes("listing transactions")) {
|
|
536
|
+
return "Failed to list transactions";
|
|
537
|
+
}
|
|
538
|
+
if (context.includes("getting transaction")) {
|
|
539
|
+
return "Failed to get transaction";
|
|
540
|
+
}
|
|
541
|
+
if (context.includes("creating transaction")) {
|
|
542
|
+
return "Failed to create transaction";
|
|
543
|
+
}
|
|
544
|
+
if (context.includes("updating transaction")) {
|
|
545
|
+
return "Failed to update transaction";
|
|
546
|
+
}
|
|
547
|
+
if (context.includes("getting user")) {
|
|
548
|
+
return "Failed to get user information";
|
|
549
|
+
}
|
|
550
|
+
return `An error occurred while ${context}`;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Extracts HTTP status code from various error shapes
|
|
555
|
+
*/
|
|
556
|
+
private extractHttpStatus(error: unknown): number | null {
|
|
557
|
+
if (!error || typeof error !== "object") {
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const directStatus = (error as { status?: unknown }).status;
|
|
562
|
+
if (
|
|
563
|
+
typeof directStatus === "number" &&
|
|
564
|
+
Number.isInteger(directStatus) &&
|
|
565
|
+
directStatus > 0
|
|
566
|
+
) {
|
|
567
|
+
return directStatus;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const response = (error as { response?: unknown }).response;
|
|
571
|
+
if (response && typeof response === "object") {
|
|
572
|
+
const responseStatus = (response as { status?: unknown }).status;
|
|
573
|
+
if (
|
|
574
|
+
typeof responseStatus === "number" &&
|
|
575
|
+
Number.isInteger(responseStatus) &&
|
|
576
|
+
responseStatus > 0
|
|
577
|
+
) {
|
|
578
|
+
return responseStatus;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Maps HTTP status codes to standardized YNAB error codes
|
|
587
|
+
*/
|
|
588
|
+
private mapHttpStatusToErrorCode(status: number): YNABErrorCode | null {
|
|
589
|
+
switch (status) {
|
|
590
|
+
case YNABErrorCode.BAD_REQUEST:
|
|
591
|
+
case YNABErrorCode.UNAUTHORIZED:
|
|
592
|
+
case YNABErrorCode.FORBIDDEN:
|
|
593
|
+
case YNABErrorCode.NOT_FOUND:
|
|
594
|
+
case YNABErrorCode.TOO_MANY_REQUESTS:
|
|
595
|
+
case YNABErrorCode.INTERNAL_SERVER_ERROR:
|
|
596
|
+
return status as YNABErrorCode;
|
|
597
|
+
default:
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Extracts sanitized details from HTTP error responses
|
|
604
|
+
*/
|
|
605
|
+
private extractHttpStatusDetails(error: unknown): string | undefined {
|
|
606
|
+
if (error && typeof error === "object") {
|
|
607
|
+
const response = (error as { response?: unknown }).response;
|
|
608
|
+
if (response && typeof response === "object") {
|
|
609
|
+
const statusText = (response as { statusText?: unknown }).statusText;
|
|
610
|
+
if (typeof statusText === "string" && statusText.trim().length > 0) {
|
|
611
|
+
return this.sanitizeErrorDetails(statusText);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (error instanceof Error && error.message) {
|
|
617
|
+
return this.sanitizeErrorDetails(error.message);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
return undefined;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Extracts structured YNAB API error information
|
|
625
|
+
*/
|
|
626
|
+
private extractYNABApiError(
|
|
627
|
+
error: unknown,
|
|
628
|
+
): { code: YNABErrorCode; details?: string } | null {
|
|
629
|
+
if (!error || typeof error !== "object") {
|
|
630
|
+
return null;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
let payload = (error as { error?: unknown }).error;
|
|
634
|
+
|
|
635
|
+
if (!payload) {
|
|
636
|
+
const responseData = (error as { response?: { data?: unknown } }).response
|
|
637
|
+
?.data;
|
|
638
|
+
if (responseData && typeof responseData === "object") {
|
|
639
|
+
payload = (responseData as { error?: unknown }).error;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (!payload || typeof payload !== "object") {
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const id = (payload as { id?: unknown }).id;
|
|
648
|
+
const name = (payload as { name?: unknown }).name;
|
|
649
|
+
const detail = (payload as { detail?: unknown }).detail;
|
|
650
|
+
|
|
651
|
+
let code: YNABErrorCode | null = null;
|
|
652
|
+
|
|
653
|
+
if (typeof id === "string") {
|
|
654
|
+
const numeric = Number.parseInt(id, 10);
|
|
655
|
+
if (!Number.isNaN(numeric)) {
|
|
656
|
+
code = this.mapHttpStatusToErrorCode(numeric);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if (!code && typeof name === "string") {
|
|
661
|
+
const normalized = name.toLowerCase();
|
|
662
|
+
if (normalized.includes("unauthorized")) {
|
|
663
|
+
code = YNABErrorCode.UNAUTHORIZED;
|
|
664
|
+
} else if (normalized.includes("forbidden")) {
|
|
665
|
+
code = YNABErrorCode.FORBIDDEN;
|
|
666
|
+
} else if (normalized.includes("not_found")) {
|
|
667
|
+
code = YNABErrorCode.NOT_FOUND;
|
|
668
|
+
} else if (
|
|
669
|
+
normalized.includes("too_many_requests") ||
|
|
670
|
+
normalized.includes("rate_limit")
|
|
671
|
+
) {
|
|
672
|
+
code = YNABErrorCode.TOO_MANY_REQUESTS;
|
|
673
|
+
} else if (normalized.includes("internal_server_error")) {
|
|
674
|
+
code = YNABErrorCode.INTERNAL_SERVER_ERROR;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
if (!code) {
|
|
679
|
+
return null;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const details = typeof detail === "string" ? detail : undefined;
|
|
683
|
+
const result: { code: YNABErrorCode; details?: string } = { code };
|
|
684
|
+
if (details !== undefined) {
|
|
685
|
+
result.details = details;
|
|
686
|
+
}
|
|
687
|
+
return result;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Sanitizes error details to prevent sensitive data leakage
|
|
692
|
+
*/
|
|
693
|
+
private sanitizeErrorDetails(error: unknown): string | undefined {
|
|
694
|
+
if (!error) return undefined;
|
|
695
|
+
|
|
696
|
+
let details = "";
|
|
697
|
+
if (error instanceof Error) {
|
|
698
|
+
details = error.message;
|
|
699
|
+
} else if (typeof error === "string") {
|
|
700
|
+
details = error;
|
|
701
|
+
} else {
|
|
702
|
+
details = "Unknown error details";
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// Remove sensitive information patterns
|
|
706
|
+
details = details
|
|
707
|
+
// token=..., token: ..., token ... → redact until delimiter or whitespace
|
|
708
|
+
.replace(/token[s]?[:\s=]+([^\s,"']+)/gi, "token=***")
|
|
709
|
+
.replace(/key[s]?[:\s=]+([^\s,"']+)/gi, "key=***")
|
|
710
|
+
.replace(/password[s]?[:\s=]+([^\s,"']+)/gi, "password=***")
|
|
711
|
+
// Authorization header (any scheme), redact rest of value
|
|
712
|
+
.replace(/authorization[:\s=]+[^\r\n]+/gi, "authorization=***")
|
|
713
|
+
// Common Bearer/JWT forms in free text
|
|
714
|
+
.replace(/\bBearer\s+[A-Za-z0-9._-]+/gi, "Bearer ***");
|
|
715
|
+
|
|
716
|
+
return details;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Wraps async functions with error handling
|
|
721
|
+
*/
|
|
722
|
+
async withErrorHandling<T>(
|
|
723
|
+
operation: () => Promise<T>,
|
|
724
|
+
context: string,
|
|
725
|
+
): Promise<T | CallToolResult> {
|
|
726
|
+
try {
|
|
727
|
+
return await operation();
|
|
728
|
+
} catch (error) {
|
|
729
|
+
return this.handleError(error, context);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Creates a validation error for invalid parameters
|
|
735
|
+
*/
|
|
736
|
+
createValidationError(
|
|
737
|
+
message: string,
|
|
738
|
+
details?: string,
|
|
739
|
+
suggestions?: string[],
|
|
740
|
+
): CallToolResult {
|
|
741
|
+
return this.handleError(
|
|
742
|
+
new ValidationError(message, details, suggestions),
|
|
743
|
+
"validating parameters",
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Creates a YNAB API error with specific error code
|
|
749
|
+
*/
|
|
750
|
+
createYNABError(
|
|
751
|
+
code: YNABErrorCode,
|
|
752
|
+
context: string,
|
|
753
|
+
originalError?: unknown,
|
|
754
|
+
): YNABAPIError {
|
|
755
|
+
const message = this.getErrorMessage(code, context);
|
|
756
|
+
return new YNABAPIError(code, message, originalError);
|
|
757
|
+
}
|
|
772
758
|
}
|
|
773
759
|
|
|
774
760
|
/**
|
|
@@ -777,28 +763,45 @@ export class ErrorHandler {
|
|
|
777
763
|
* @param formatter - Formatter used to convert structured error responses into strings for tool output
|
|
778
764
|
* @returns A new ErrorHandler configured to use the provided `formatter`
|
|
779
765
|
*/
|
|
780
|
-
export function createErrorHandler(
|
|
781
|
-
|
|
766
|
+
export function createErrorHandler(
|
|
767
|
+
formatter: ErrorResponseFormatter,
|
|
768
|
+
): ErrorHandler {
|
|
769
|
+
return new ErrorHandler(formatter);
|
|
782
770
|
}
|
|
783
771
|
|
|
772
|
+
/**
|
|
773
|
+
* Module-level fallback ErrorHandler for standalone functions when no instance
|
|
774
|
+
* is provided. Uses a simple JSON formatter.
|
|
775
|
+
*/
|
|
776
|
+
const fallbackErrorHandler = new ErrorHandler({
|
|
777
|
+
format: (value: unknown) => JSON.stringify(value, null, 2),
|
|
778
|
+
});
|
|
779
|
+
|
|
784
780
|
/**
|
|
785
781
|
* Utility function for handling errors in tool handlers
|
|
786
782
|
*/
|
|
787
783
|
export function handleToolError(
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
784
|
+
error: unknown,
|
|
785
|
+
toolName: string,
|
|
786
|
+
operation: string,
|
|
787
|
+
errorHandler?: ErrorHandler,
|
|
791
788
|
): CallToolResult {
|
|
792
|
-
|
|
789
|
+
const eh = errorHandler ?? fallbackErrorHandler;
|
|
790
|
+
return eh.handleError(error, `executing ${toolName} - ${operation}`);
|
|
793
791
|
}
|
|
794
792
|
|
|
795
793
|
/**
|
|
796
794
|
* Utility function for wrapping tool operations with error handling
|
|
797
795
|
*/
|
|
798
796
|
export async function withToolErrorHandling<T>(
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
797
|
+
operation: () => Promise<T>,
|
|
798
|
+
toolName: string,
|
|
799
|
+
operationName: string,
|
|
800
|
+
errorHandler?: ErrorHandler,
|
|
802
801
|
): Promise<T | CallToolResult> {
|
|
803
|
-
|
|
802
|
+
const eh = errorHandler ?? fallbackErrorHandler;
|
|
803
|
+
return eh.withErrorHandling(
|
|
804
|
+
operation,
|
|
805
|
+
`executing ${toolName} - ${operationName}`,
|
|
806
|
+
);
|
|
804
807
|
}
|