@guiie/buda-mcp 1.5.0 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/.cursor/rules/marketplace-docs-sync.mdc +32 -0
  2. package/CHANGELOG.md +75 -0
  3. package/PUBLISH_CHECKLIST.md +48 -89
  4. package/README.md +446 -78
  5. package/dist/audit.d.ts +21 -0
  6. package/dist/audit.d.ts.map +1 -0
  7. package/dist/audit.js +14 -0
  8. package/dist/client.d.ts +1 -0
  9. package/dist/client.d.ts.map +1 -1
  10. package/dist/client.js +2 -1
  11. package/dist/http.js +65 -7
  12. package/dist/index.js +12 -3
  13. package/dist/tools/account.js +1 -1
  14. package/dist/tools/arbitrage.js +1 -1
  15. package/dist/tools/balance.js +1 -1
  16. package/dist/tools/balances.js +1 -1
  17. package/dist/tools/banks.js +1 -1
  18. package/dist/tools/batch_orders.d.ts +6 -1
  19. package/dist/tools/batch_orders.d.ts.map +1 -1
  20. package/dist/tools/batch_orders.js +47 -3
  21. package/dist/tools/cancel_all_orders.d.ts +1 -1
  22. package/dist/tools/cancel_all_orders.d.ts.map +1 -1
  23. package/dist/tools/cancel_all_orders.js +10 -13
  24. package/dist/tools/cancel_order.d.ts +1 -1
  25. package/dist/tools/cancel_order.d.ts.map +1 -1
  26. package/dist/tools/cancel_order.js +10 -10
  27. package/dist/tools/cancel_order_by_client_id.d.ts +1 -1
  28. package/dist/tools/cancel_order_by_client_id.d.ts.map +1 -1
  29. package/dist/tools/cancel_order_by_client_id.js +9 -9
  30. package/dist/tools/compare_markets.d.ts +9 -0
  31. package/dist/tools/compare_markets.d.ts.map +1 -1
  32. package/dist/tools/compare_markets.js +63 -53
  33. package/dist/tools/dead_mans_switch.d.ts +2 -2
  34. package/dist/tools/dead_mans_switch.d.ts.map +1 -1
  35. package/dist/tools/dead_mans_switch.js +68 -6
  36. package/dist/tools/deposits.js +2 -2
  37. package/dist/tools/fees.js +1 -1
  38. package/dist/tools/lightning.d.ts +1 -1
  39. package/dist/tools/lightning.d.ts.map +1 -1
  40. package/dist/tools/lightning.js +25 -9
  41. package/dist/tools/market_sentiment.js +1 -1
  42. package/dist/tools/market_summary.js +1 -1
  43. package/dist/tools/markets.js +1 -1
  44. package/dist/tools/order_lookup.js +2 -2
  45. package/dist/tools/orderbook.js +1 -1
  46. package/dist/tools/orders.js +1 -1
  47. package/dist/tools/place_order.d.ts +1 -1
  48. package/dist/tools/place_order.d.ts.map +1 -1
  49. package/dist/tools/place_order.js +53 -4
  50. package/dist/tools/price_history.js +1 -1
  51. package/dist/tools/quotation.js +1 -1
  52. package/dist/tools/receive_addresses.d.ts +6 -1
  53. package/dist/tools/receive_addresses.d.ts.map +1 -1
  54. package/dist/tools/receive_addresses.js +37 -13
  55. package/dist/tools/remittance_recipients.js +2 -2
  56. package/dist/tools/remittances.d.ts +7 -2
  57. package/dist/tools/remittances.d.ts.map +1 -1
  58. package/dist/tools/remittances.js +46 -23
  59. package/dist/tools/simulate_order.js +1 -1
  60. package/dist/tools/spread.js +1 -1
  61. package/dist/tools/technical_indicators.d.ts.map +1 -1
  62. package/dist/tools/technical_indicators.js +3 -2
  63. package/dist/tools/ticker.js +1 -1
  64. package/dist/tools/trades.js +1 -1
  65. package/dist/tools/volume.js +1 -1
  66. package/dist/tools/withdrawals.d.ts +1 -1
  67. package/dist/tools/withdrawals.d.ts.map +1 -1
  68. package/dist/tools/withdrawals.js +21 -11
  69. package/dist/utils.d.ts +10 -0
  70. package/dist/utils.d.ts.map +1 -1
  71. package/dist/utils.js +29 -1
  72. package/dist/validation.d.ts +6 -0
  73. package/dist/validation.d.ts.map +1 -1
  74. package/dist/validation.js +26 -0
  75. package/dist/version.d.ts.map +1 -1
  76. package/dist/version.js +8 -1
  77. package/marketplace/README.md +1 -1
  78. package/marketplace/claude-listing.md +75 -4
  79. package/marketplace/gemini-tools.json +325 -2
  80. package/marketplace/openapi.yaml +160 -1
  81. package/package.json +2 -1
  82. package/server.json +2 -2
  83. package/src/audit.ts +24 -0
  84. package/src/client.ts +3 -1
  85. package/src/http.ts +75 -7
  86. package/src/index.ts +10 -3
  87. package/src/tools/account.ts +1 -1
  88. package/src/tools/arbitrage.ts +1 -1
  89. package/src/tools/balance.ts +1 -1
  90. package/src/tools/balances.ts +1 -1
  91. package/src/tools/banks.ts +1 -1
  92. package/src/tools/batch_orders.ts +52 -2
  93. package/src/tools/cancel_all_orders.ts +10 -12
  94. package/src/tools/cancel_order.ts +10 -9
  95. package/src/tools/cancel_order_by_client_id.ts +9 -8
  96. package/src/tools/compare_markets.ts +78 -61
  97. package/src/tools/dead_mans_switch.ts +76 -5
  98. package/src/tools/deposits.ts +2 -2
  99. package/src/tools/fees.ts +1 -1
  100. package/src/tools/lightning.ts +28 -9
  101. package/src/tools/market_sentiment.ts +1 -1
  102. package/src/tools/market_summary.ts +1 -1
  103. package/src/tools/markets.ts +1 -1
  104. package/src/tools/order_lookup.ts +2 -2
  105. package/src/tools/orderbook.ts +1 -1
  106. package/src/tools/orders.ts +1 -1
  107. package/src/tools/place_order.ts +56 -5
  108. package/src/tools/price_history.ts +1 -1
  109. package/src/tools/quotation.ts +1 -1
  110. package/src/tools/receive_addresses.ts +40 -13
  111. package/src/tools/remittance_recipients.ts +2 -2
  112. package/src/tools/remittances.ts +49 -22
  113. package/src/tools/simulate_order.ts +1 -1
  114. package/src/tools/spread.ts +1 -1
  115. package/src/tools/technical_indicators.ts +3 -2
  116. package/src/tools/ticker.ts +1 -1
  117. package/src/tools/trades.ts +1 -1
  118. package/src/tools/volume.ts +1 -1
  119. package/src/tools/withdrawals.ts +22 -10
  120. package/src/utils.ts +36 -1
  121. package/src/validation.ts +29 -0
  122. package/src/version.ts +11 -3
  123. package/test/unit.ts +623 -22
package/test/unit.ts CHANGED
@@ -6,10 +6,11 @@
6
6
  import { createHmac } from "crypto";
7
7
  import { BudaClient, BudaApiError } from "../src/client.js";
8
8
  import { MemoryCache } from "../src/cache.js";
9
- import { validateMarketId } from "../src/validation.js";
9
+ import { validateMarketId, validateCryptoAddress } from "../src/validation.js";
10
10
  import { handlePlaceOrder } from "../src/tools/place_order.js";
11
11
  import { handleCancelOrder } from "../src/tools/cancel_order.js";
12
- import { flattenAmount, getLiquidityRating, aggregateTradesToCandles } from "../src/utils.js";
12
+ import { flattenAmount, getLiquidityRating, aggregateTradesToCandles, safeTokenEqual, parseEnvInt } from "../src/utils.js";
13
+ import { logAudit } from "../src/audit.js";
13
14
  import { handleArbitrageOpportunities } from "../src/tools/arbitrage.js";
14
15
  import { handleMarketSummary } from "../src/tools/market_summary.js";
15
16
  import { handleSimulateOrder } from "../src/tools/simulate_order.js";
@@ -35,6 +36,7 @@ import { handlePlaceBatchOrders } from "../src/tools/batch_orders.js";
35
36
  import { handleCreateWithdrawal } from "../src/tools/withdrawals.js";
36
37
  import { handleCreateFiatDeposit } from "../src/tools/deposits.js";
37
38
  import { handleLightningWithdrawal, handleCreateLightningInvoice } from "../src/tools/lightning.js";
39
+ import { handleCompareMarkets } from "../src/tools/compare_markets.js";
38
40
 
39
41
  // ----------------------------------------------------------------
40
42
  // Minimal test harness
@@ -1911,7 +1913,7 @@ await test("handleCreateReceiveAddress: INVALID_CURRENCY without fetch", async (
1911
1913
  };
1912
1914
  try {
1913
1915
  const client = {} as BudaClient;
1914
- const result = await handleCreateReceiveAddress({ currency: "!!!!" }, client);
1916
+ const result = await handleCreateReceiveAddress({ currency: "!!!!", confirmation_token: "CONFIRM" }, client);
1915
1917
  assert(result.isError === true, "should be error");
1916
1918
  assert(!fetchCalled.value, "fetch should not have been called");
1917
1919
  const parsed = JSON.parse(result.content[0].text) as { code: string };
@@ -1938,7 +1940,7 @@ await test("handleCreateReceiveAddress: happy path", async () => {
1938
1940
  );
1939
1941
  try {
1940
1942
  const client = new BudaClient("https://www.buda.com/api/v2");
1941
- const result = await handleCreateReceiveAddress({ currency: "BTC" }, client);
1943
+ const result = await handleCreateReceiveAddress({ currency: "BTC", confirmation_token: "CONFIRM" }, client);
1942
1944
  assert(!result.isError, "should not be error");
1943
1945
  const parsed = JSON.parse(result.content[0].text) as {
1944
1946
  id: number;
@@ -1961,7 +1963,7 @@ await test("handleCreateReceiveAddress: fiat currency API error passthrough", as
1961
1963
  new Response(JSON.stringify({ message: "Not found" }), { status: 404 });
1962
1964
  try {
1963
1965
  const client = new BudaClient("https://www.buda.com/api/v2");
1964
- const result = await handleCreateReceiveAddress({ currency: "CLP" }, client);
1966
+ const result = await handleCreateReceiveAddress({ currency: "CLP", confirmation_token: "CONFIRM" }, client);
1965
1967
  assert(result.isError === true, "should be error for fiat");
1966
1968
  const parsed = JSON.parse(result.content[0].text) as { code: number };
1967
1969
  assertEqual(parsed.code, 404, "code should be 404");
@@ -1985,7 +1987,7 @@ await test("handleQuoteRemittance: INVALID_CURRENCY without fetch", async () =>
1985
1987
  };
1986
1988
  try {
1987
1989
  const client = {} as BudaClient;
1988
- const result = await handleQuoteRemittance({ currency: "!!!", amount: 100, recipient_id: 1 }, client);
1990
+ const result = await handleQuoteRemittance({ currency: "!!!", amount: 100, recipient_id: 1, confirmation_token: "CONFIRM" }, client);
1989
1991
  assert(result.isError === true, "should be error");
1990
1992
  assert(!fetchCalled.value, "fetch should not have been called");
1991
1993
  const parsed = JSON.parse(result.content[0].text) as { code: string };
@@ -2014,7 +2016,7 @@ await test("handleQuoteRemittance: happy path", async () => {
2014
2016
  );
2015
2017
  try {
2016
2018
  const client = new BudaClient("https://www.buda.com/api/v2");
2017
- const result = await handleQuoteRemittance({ currency: "CLP", amount: 100000, recipient_id: 5 }, client);
2019
+ const result = await handleQuoteRemittance({ currency: "CLP", amount: 100000, recipient_id: 5, confirmation_token: "CONFIRM" }, client);
2018
2020
  assert(!result.isError, "should not be error");
2019
2021
  const parsed = JSON.parse(result.content[0].text) as {
2020
2022
  id: number;
@@ -2035,7 +2037,7 @@ await test("handleQuoteRemittance: 404 unknown recipient passthrough", async ()
2035
2037
  new Response(JSON.stringify({ message: "Not found" }), { status: 404 });
2036
2038
  try {
2037
2039
  const client = new BudaClient("https://www.buda.com/api/v2");
2038
- const result = await handleQuoteRemittance({ currency: "CLP", amount: 100000, recipient_id: 9999 }, client);
2040
+ const result = await handleQuoteRemittance({ currency: "CLP", amount: 100000, recipient_id: 9999, confirmation_token: "CONFIRM" }, client);
2039
2041
  assert(result.isError === true, "should be error");
2040
2042
  const parsed = JSON.parse(result.content[0].text) as { code: number };
2041
2043
  assertEqual(parsed.code, 404, "code should be 404");
@@ -2732,19 +2734,19 @@ await test("handleCreateWithdrawal: crypto path + CONFIRM", async () => {
2732
2734
  currency: "BTC",
2733
2735
  amount: ["0.01", "BTC"],
2734
2736
  fee: ["0.0001", "BTC"],
2735
- address: "bc1q...",
2736
- tx_hash: null,
2737
- bank_account_id: null,
2738
- created_at: "2024-01-01T00:00:00Z",
2739
- updated_at: "2024-01-01T00:00:00Z",
2740
- },
2741
- }),
2742
- { status: 201, headers: { "Content-Type": "application/json" } },
2743
- );
2737
+ address: "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
2738
+ tx_hash: null,
2739
+ bank_account_id: null,
2740
+ created_at: "2024-01-01T00:00:00Z",
2741
+ updated_at: "2024-01-01T00:00:00Z",
2742
+ },
2743
+ }),
2744
+ { status: 201, headers: { "Content-Type": "application/json" } },
2745
+ );
2744
2746
  try {
2745
2747
  const client = new BudaClient("https://www.buda.com/api/v2");
2746
2748
  const result = await handleCreateWithdrawal(
2747
- { currency: "BTC", amount: 0.01, address: "bc1q...", confirmation_token: "CONFIRM" },
2749
+ { currency: "BTC", amount: 0.01, address: "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq", confirmation_token: "CONFIRM" },
2748
2750
  client,
2749
2751
  );
2750
2752
  assert(!result.isError, "should not be error");
@@ -2804,7 +2806,7 @@ await test("handleCreateWithdrawal: 422 insufficient balance passthrough", async
2804
2806
  try {
2805
2807
  const client = new BudaClient("https://www.buda.com/api/v2");
2806
2808
  const result = await handleCreateWithdrawal(
2807
- { currency: "BTC", amount: 999, address: "bc1q...", confirmation_token: "CONFIRM" },
2809
+ { currency: "BTC", amount: 999, address: "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq", confirmation_token: "CONFIRM" },
2808
2810
  client,
2809
2811
  );
2810
2812
  assert(result.isError === true, "should be error");
@@ -2934,7 +2936,7 @@ await test("handleLightningWithdrawal: missing/wrong token returns CONFIRMATION_
2934
2936
  try {
2935
2937
  const client = {} as BudaClient;
2936
2938
  const result = await handleLightningWithdrawal(
2937
- { invoice: "lnbc1000u1ptest...", confirmation_token: "NOPE" },
2939
+ { invoice: "lnbc1000u1p" + "a".repeat(40), confirmation_token: "NOPE" },
2938
2940
  client,
2939
2941
  );
2940
2942
  assert(result.isError === true, "should be error");
@@ -2966,7 +2968,7 @@ await test("handleLightningWithdrawal: happy path returns flat withdrawal", asyn
2966
2968
  try {
2967
2969
  const client = new BudaClient("https://www.buda.com/api/v2");
2968
2970
  const result = await handleLightningWithdrawal(
2969
- { invoice: "lnbc1000u1ptest...", confirmation_token: "CONFIRM" },
2971
+ { invoice: "lnbc1000u1p" + "a".repeat(40), confirmation_token: "CONFIRM" },
2970
2972
  client,
2971
2973
  );
2972
2974
  assert(!result.isError, "should not be error");
@@ -2992,7 +2994,7 @@ await test("handleLightningWithdrawal: API error passthrough", async () => {
2992
2994
  try {
2993
2995
  const client = new BudaClient("https://www.buda.com/api/v2");
2994
2996
  const result = await handleLightningWithdrawal(
2995
- { invoice: "lnbc1000u1ptest...", confirmation_token: "CONFIRM" },
2997
+ { invoice: "lnbc1000u1p" + "a".repeat(40), confirmation_token: "CONFIRM" },
2996
2998
  client,
2997
2999
  );
2998
3000
  assert(result.isError === true, "should be error");
@@ -3064,6 +3066,605 @@ await test("handleCreateLightningInvoice: API error passthrough", async () => {
3064
3066
  }
3065
3067
  });
3066
3068
 
3069
+ // ----------------------------------------------------------------
3070
+ // Fix 3 — validateCryptoAddress
3071
+ // ----------------------------------------------------------------
3072
+
3073
+ section("validateCryptoAddress");
3074
+
3075
+ await test("validateCryptoAddress: valid BTC bech32 address passes", () => {
3076
+ assertEqual(validateCryptoAddress("bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq", "BTC"), null, "valid bech32 should pass");
3077
+ });
3078
+
3079
+ await test("validateCryptoAddress: valid BTC legacy P2PKH address passes", () => {
3080
+ assertEqual(validateCryptoAddress("1BpEi6DfDAUFd153wiGrvkiKyhSua3FrN", "BTC"), null, "valid P2PKH should pass");
3081
+ });
3082
+
3083
+ await test("validateCryptoAddress: BTC address too short after bc1 prefix is rejected", () => {
3084
+ // bc1 + fewer than 6 alphanumeric chars — too short to be a real address
3085
+ assert(validateCryptoAddress("bc1qa", "BTC") !== null, "bc1 + 1 char should fail");
3086
+ });
3087
+
3088
+ await test("validateCryptoAddress: BTC address with wrong prefix is rejected", () => {
3089
+ assert(validateCryptoAddress("XpubBadAddress1234567890abcdefgh", "BTC") !== null, "wrong prefix should fail");
3090
+ });
3091
+
3092
+ await test("validateCryptoAddress: valid ETH address passes", () => {
3093
+ // Standard 40-hex-char Ethereum address (checksummed)
3094
+ assertEqual(validateCryptoAddress("0x742d35Cc6634C0532925a3b844Bc454e4438f44e", "ETH"), null, "valid ETH address should pass");
3095
+ });
3096
+
3097
+ await test("validateCryptoAddress: ETH address wrong length is rejected", () => {
3098
+ assert(validateCryptoAddress("0xde0B295669a9FD93d5F28D9", "ETH") !== null, "short ETH address should fail");
3099
+ });
3100
+
3101
+ await test("validateCryptoAddress: valid XRP address passes", () => {
3102
+ assertEqual(validateCryptoAddress("rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", "XRP"), null, "valid XRP address should pass");
3103
+ });
3104
+
3105
+ await test("validateCryptoAddress: XRP address with wrong prefix is rejected", () => {
3106
+ assert(validateCryptoAddress("xHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", "XRP") !== null, "XRP with 'x' prefix should fail");
3107
+ });
3108
+
3109
+ await test("validateCryptoAddress: unknown currency returns null (pass-through)", () => {
3110
+ // ALGO not in ADDRESS_RULES — should pass through to let exchange validate
3111
+ assertEqual(validateCryptoAddress("SOMEADDRESS123", "ALGO"), null, "unknown currency should pass through");
3112
+ });
3113
+
3114
+ await test("validateCryptoAddress: USDC treated as EVM address", () => {
3115
+ assertEqual(validateCryptoAddress("0x742d35Cc6634C0532925a3b844Bc454e4438f44e", "USDC"), null, "valid EVM address for USDC should pass");
3116
+ assert(validateCryptoAddress("not-an-address", "USDC") !== null, "invalid EVM address for USDC should fail");
3117
+ });
3118
+
3119
+ // ----------------------------------------------------------------
3120
+ // Fix 4 — handleLightningWithdrawal BOLT-11 validation
3121
+ // ----------------------------------------------------------------
3122
+
3123
+ section("handleLightningWithdrawal — BOLT-11 format guard");
3124
+
3125
+ await test("handleLightningWithdrawal: non-BOLT11 string returns INVALID_INVOICE before API call", async () => {
3126
+ let fetchCalled = false;
3127
+ const savedFetch = globalThis.fetch;
3128
+ globalThis.fetch = async () => {
3129
+ fetchCalled = true;
3130
+ return new Response("{}", { status: 200 });
3131
+ };
3132
+ try {
3133
+ const client = new BudaClient(undefined, "key", "secret");
3134
+ const result = await handleLightningWithdrawal(
3135
+ { invoice: "not-a-lightning-invoice-at-all-just-garbage-string-here", confirmation_token: "CONFIRM" },
3136
+ client,
3137
+ );
3138
+ const parsed = JSON.parse(result.content[0].text) as { code: string };
3139
+ assertEqual(parsed.code, "INVALID_INVOICE", "should return INVALID_INVOICE");
3140
+ assert(result.isError === true, "isError should be true");
3141
+ assert(!fetchCalled, "fetch should NOT have been called");
3142
+ } finally {
3143
+ globalThis.fetch = savedFetch;
3144
+ }
3145
+ });
3146
+
3147
+ await test("handleLightningWithdrawal: testnet invoice prefix 'lntb' is accepted (passes format check)", async () => {
3148
+ // A syntactically valid lntb prefix — API will reject it, but format guard should pass
3149
+ const savedFetch = globalThis.fetch;
3150
+ globalThis.fetch = async () => new Response(
3151
+ JSON.stringify({ message: "invalid invoice" }), { status: 422 },
3152
+ );
3153
+ try {
3154
+ const client = new BudaClient(undefined, "key", "secret");
3155
+ const fakeInvoice = "lntb1230n1pj8ygappp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypq";
3156
+ const result = await handleLightningWithdrawal(
3157
+ { invoice: fakeInvoice, confirmation_token: "CONFIRM" },
3158
+ client,
3159
+ );
3160
+ // Should NOT return INVALID_INVOICE — the format guard passes; API error is expected
3161
+ const parsed = JSON.parse(result.content[0].text) as { code: string };
3162
+ assert(parsed.code !== "INVALID_INVOICE", "BOLT-11 prefix should pass format guard");
3163
+ } finally {
3164
+ globalThis.fetch = savedFetch;
3165
+ }
3166
+ });
3167
+
3168
+ await test("handleLightningWithdrawal: invoice without BOLT-11 prefix still blocked by INVALID_INVOICE after CONFIRM", async () => {
3169
+ const client = new BudaClient(undefined, "key", "secret");
3170
+ const result = await handleLightningWithdrawal(
3171
+ { invoice: "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq", confirmation_token: "CONFIRM" },
3172
+ client,
3173
+ );
3174
+ const parsed = JSON.parse(result.content[0].text) as { code: string };
3175
+ assertEqual(parsed.code, "INVALID_INVOICE", "BTC address passed as invoice should fail BOLT-11 check");
3176
+ });
3177
+
3178
+ // ----------------------------------------------------------------
3179
+ // Fix 5 — Dead man's switch: TRANSPORT_NOT_SUPPORTED on HTTP
3180
+ // ----------------------------------------------------------------
3181
+
3182
+ section("handleScheduleCancelAll — HTTP transport guard");
3183
+
3184
+ await test("handleScheduleCancelAll: transport='http' returns TRANSPORT_NOT_SUPPORTED", async () => {
3185
+ const client = new BudaClient(undefined, "key", "secret");
3186
+ // Simulate what register() does on HTTP transport by calling handleScheduleCancelAll
3187
+ // We test the transport guard via the register() wrapper indirectly through a fake server tool.
3188
+ // Since handleScheduleCancelAll itself doesn't know the transport, we test the guard
3189
+ // by verifying the pattern: a fake dispatch that mirrors the http register() closure.
3190
+ const transportGuard = (transport: "stdio" | "http") => {
3191
+ if (transport === "http") {
3192
+ return Promise.resolve({
3193
+ content: [{ type: "text" as const, text: JSON.stringify({ code: "TRANSPORT_NOT_SUPPORTED" }) }],
3194
+ isError: true,
3195
+ });
3196
+ }
3197
+ return handleScheduleCancelAll(
3198
+ { market_id: "BTC-CLP", ttl_seconds: 30, confirmation_token: "CONFIRM" },
3199
+ client,
3200
+ );
3201
+ };
3202
+
3203
+ const result = await transportGuard("http");
3204
+ const parsed = JSON.parse(result.content[0].text) as { code: string };
3205
+ assertEqual(parsed.code, "TRANSPORT_NOT_SUPPORTED", "HTTP transport should return TRANSPORT_NOT_SUPPORTED");
3206
+ assert(result.isError === true, "isError should be true");
3207
+ });
3208
+
3209
+ await test("handleScheduleCancelAll: transport='stdio' still works normally", async () => {
3210
+ const client = new BudaClient(undefined, "key", "secret");
3211
+ const result = await handleScheduleCancelAll(
3212
+ { market_id: "BTC-CLP", ttl_seconds: 10, confirmation_token: "CONFIRM" },
3213
+ client,
3214
+ );
3215
+ const parsed = JSON.parse(result.content[0].text) as { active: boolean; warning: string };
3216
+ assert(parsed.active === true, "stdio transport should arm the switch");
3217
+ assert(typeof parsed.warning === "string", "should include in-memory warning");
3218
+ // Clean up timer
3219
+ const { handleDisarmCancelTimer: disarm } = await import("../src/tools/dead_mans_switch.js");
3220
+ disarm({ market_id: "BTC-CLP" });
3221
+ });
3222
+
3223
+ // ----------------------------------------------------------------
3224
+ // Fix 6 — place_batch_orders: max_notional cap
3225
+ // ----------------------------------------------------------------
3226
+
3227
+ section("handlePlaceBatchOrders — max_notional cap");
3228
+
3229
+ await test("handlePlaceBatchOrders: exceeding max_notional rejects before any API call", async () => {
3230
+ let fetchCalled = false;
3231
+ const savedFetch = globalThis.fetch;
3232
+ globalThis.fetch = async () => {
3233
+ fetchCalled = true;
3234
+ return new Response("{}", { status: 200 });
3235
+ };
3236
+ try {
3237
+ const client = new BudaClient(undefined, "key", "secret");
3238
+ const result = await handlePlaceBatchOrders(
3239
+ {
3240
+ orders: [
3241
+ { market_id: "BTC-CLP", type: "Bid", price_type: "limit", amount: 1, limit_price: 50_000_000 },
3242
+ { market_id: "BTC-CLP", type: "Bid", price_type: "limit", amount: 0.5, limit_price: 50_000_000 },
3243
+ ],
3244
+ max_notional: 60_000_000,
3245
+ confirmation_token: "CONFIRM",
3246
+ },
3247
+ client,
3248
+ );
3249
+ // total notional = 1*50_000_000 + 0.5*50_000_000 = 75_000_000 > 60_000_000
3250
+ const parsed = JSON.parse(result.content[0].text) as { code: string; total_notional: number };
3251
+ assertEqual(parsed.code, "NOTIONAL_CAP_EXCEEDED", "should return NOTIONAL_CAP_EXCEEDED");
3252
+ assert(parsed.total_notional === 75_000_000, "total_notional should be computed correctly");
3253
+ assert(result.isError === true, "isError should be true");
3254
+ assert(!fetchCalled, "fetch should NOT have been called");
3255
+ } finally {
3256
+ globalThis.fetch = savedFetch;
3257
+ }
3258
+ });
3259
+
3260
+ await test("handlePlaceBatchOrders: within max_notional proceeds to API calls", async () => {
3261
+ const savedFetch = globalThis.fetch;
3262
+ globalThis.fetch = async () => new Response(
3263
+ JSON.stringify({ order: { id: 99, state: "pending", market_id: "btc-clp" } }),
3264
+ { status: 200 },
3265
+ );
3266
+ try {
3267
+ const client = new BudaClient(undefined, "key", "secret");
3268
+ const result = await handlePlaceBatchOrders(
3269
+ {
3270
+ orders: [
3271
+ { market_id: "BTC-CLP", type: "Bid", price_type: "limit", amount: 0.001, limit_price: 50_000_000 },
3272
+ ],
3273
+ max_notional: 100_000,
3274
+ confirmation_token: "CONFIRM",
3275
+ },
3276
+ client,
3277
+ );
3278
+ // total notional = 0.001 * 50_000_000 = 50_000 < 100_000 — should proceed
3279
+ const parsed = JSON.parse(result.content[0].text) as { succeeded: number };
3280
+ assertEqual(parsed.succeeded, 1, "order within cap should succeed");
3281
+ } finally {
3282
+ globalThis.fetch = savedFetch;
3283
+ }
3284
+ });
3285
+
3286
+ await test("handlePlaceBatchOrders: market orders contribute 0 to notional (cap not triggered)", async () => {
3287
+ const savedFetch = globalThis.fetch;
3288
+ globalThis.fetch = async () => new Response(
3289
+ JSON.stringify({ order: { id: 100, state: "pending", market_id: "btc-clp" } }),
3290
+ { status: 200 },
3291
+ );
3292
+ try {
3293
+ const client = new BudaClient(undefined, "key", "secret");
3294
+ const result = await handlePlaceBatchOrders(
3295
+ {
3296
+ orders: [
3297
+ { market_id: "BTC-CLP", type: "Bid", price_type: "market", amount: 999 },
3298
+ ],
3299
+ max_notional: 1,
3300
+ confirmation_token: "CONFIRM",
3301
+ },
3302
+ client,
3303
+ );
3304
+ // Market order contributes 0, so notional = 0 < 1 — cap should NOT trigger
3305
+ const parsed = JSON.parse(result.content[0].text) as { code?: string; succeeded?: number };
3306
+ assert(parsed.code !== "NOTIONAL_CAP_EXCEEDED", "market order should not trigger notional cap");
3307
+ } finally {
3308
+ globalThis.fetch = savedFetch;
3309
+ }
3310
+ });
3311
+
3312
+ // ----------------------------------------------------------------
3313
+ // Security — audit logging (logAudit)
3314
+ // ----------------------------------------------------------------
3315
+
3316
+ section("logAudit — structured audit logging");
3317
+
3318
+ await test("logAudit: writes valid JSON with required fields to stderr", () => {
3319
+ const lines: string[] = [];
3320
+ const savedWrite = process.stderr.write.bind(process.stderr);
3321
+ (process.stderr as unknown as { write: (s: string) => boolean }).write = (s: string) => {
3322
+ lines.push(s);
3323
+ return true;
3324
+ };
3325
+ try {
3326
+ logAudit({ ts: "2024-01-01T00:00:00.000Z", tool: "place_order", transport: "stdio", args_summary: { market_id: "BTC-CLP" }, success: true });
3327
+ } finally {
3328
+ (process.stderr as unknown as { write: (s: string) => boolean }).write = savedWrite;
3329
+ }
3330
+ assert(lines.length === 1, "should write exactly one line");
3331
+ const parsed = JSON.parse(lines[0].trim()) as Record<string, unknown>;
3332
+ assert(parsed.audit === true, "must have audit:true marker");
3333
+ assertEqual(parsed.tool as string, "place_order", "tool field must be present");
3334
+ assertEqual(parsed.transport as string, "stdio", "transport field must be present");
3335
+ assert(typeof parsed.ts === "string", "ts field must be a string");
3336
+ assert(typeof parsed.success === "boolean", "success field must be a boolean");
3337
+ });
3338
+
3339
+ await test("logAudit: output does NOT contain confirmation_token", () => {
3340
+ const lines: string[] = [];
3341
+ const savedWrite = process.stderr.write.bind(process.stderr);
3342
+ (process.stderr as unknown as { write: (s: string) => boolean }).write = (s: string) => {
3343
+ lines.push(s);
3344
+ return true;
3345
+ };
3346
+ try {
3347
+ logAudit({ ts: new Date().toISOString(), tool: "place_order", transport: "stdio", args_summary: { market_id: "BTC-CLP", type: "Bid" }, success: false, error_code: "CONFIRMATION_REQUIRED" });
3348
+ } finally {
3349
+ (process.stderr as unknown as { write: (s: string) => boolean }).write = savedWrite;
3350
+ }
3351
+ const raw = lines[0] ?? "";
3352
+ assert(!raw.includes("confirmation_token"), "audit log must NOT contain confirmation_token");
3353
+ assert(!raw.includes("invoice"), "audit log must NOT contain invoice field");
3354
+ });
3355
+
3356
+ await test("logAudit: error_code is included when success is false", () => {
3357
+ const lines: string[] = [];
3358
+ const savedWrite = process.stderr.write.bind(process.stderr);
3359
+ (process.stderr as unknown as { write: (s: string) => boolean }).write = (s: string) => {
3360
+ lines.push(s);
3361
+ return true;
3362
+ };
3363
+ try {
3364
+ logAudit({ ts: new Date().toISOString(), tool: "cancel_order", transport: "http", args_summary: {}, success: false, error_code: 422 });
3365
+ } finally {
3366
+ (process.stderr as unknown as { write: (s: string) => boolean }).write = savedWrite;
3367
+ }
3368
+ const parsed = JSON.parse(lines[0].trim()) as Record<string, unknown>;
3369
+ assertEqual(parsed.error_code as number, 422, "error_code must be 422");
3370
+ assertEqual(parsed.success as boolean, false, "success must be false");
3371
+ });
3372
+
3373
+ await test("handlePlaceOrder with CONFIRM → logAudit called with success:true", async () => {
3374
+ const savedFetch = globalThis.fetch;
3375
+ const auditLines: string[] = [];
3376
+ const savedWrite = process.stderr.write.bind(process.stderr);
3377
+ (process.stderr as unknown as { write: (s: string) => boolean }).write = (s: string) => {
3378
+ if (s.includes('"audit":true')) auditLines.push(s);
3379
+ return true;
3380
+ };
3381
+ globalThis.fetch = async () => new Response(
3382
+ JSON.stringify({ order: { id: 1, state: "pending", market_id: "btc-clp", type: "Bid", price_type: "limit", amount: ["0.001", "BTC"], original_amount: ["0.001", "BTC"], traded_amount: ["0", "BTC"], total_exchanged: ["0", "CLP"], paid_fee: ["0", "CLP"], limit: ["80000000", "CLP"] } }),
3383
+ { status: 200 },
3384
+ );
3385
+ try {
3386
+ const client = new BudaClient(undefined, "key", "secret");
3387
+ const result = await handlePlaceOrder(
3388
+ { market_id: "BTC-CLP", type: "Bid", price_type: "limit", amount: 0.001, limit_price: 80_000_000, confirmation_token: "CONFIRM" },
3389
+ client,
3390
+ );
3391
+ assert(!result.isError, "should succeed");
3392
+ assert(auditLines.length > 0, "logAudit must have been called");
3393
+ const parsed = JSON.parse(auditLines[0].trim()) as Record<string, unknown>;
3394
+ assertEqual(parsed.tool as string, "place_order", "tool should be place_order");
3395
+ assert(parsed.success === true, "success should be true");
3396
+ } finally {
3397
+ globalThis.fetch = savedFetch;
3398
+ (process.stderr as unknown as { write: (s: string) => boolean }).write = savedWrite;
3399
+ }
3400
+ });
3401
+
3402
+ // ----------------------------------------------------------------
3403
+ // Security — error response does not expose API path
3404
+ // ----------------------------------------------------------------
3405
+
3406
+ section("Error responses — path field redacted");
3407
+
3408
+ await test("handlePlaceOrder with invalid market: error response has no 'path' field", async () => {
3409
+ const client = new BudaClient(undefined, "key", "secret");
3410
+ const result = await handlePlaceOrder(
3411
+ { market_id: "INVALID!!!", type: "Bid", price_type: "limit", amount: 1, limit_price: 1, confirmation_token: "CONFIRM" },
3412
+ client,
3413
+ );
3414
+ assert(result.isError === true, "should be error");
3415
+ const parsed = JSON.parse(result.content[0].text) as Record<string, unknown>;
3416
+ assert(!("path" in parsed), "error response must NOT contain 'path' field");
3417
+ });
3418
+
3419
+ await test("handleCancelAllOrders API error: response has no 'path' field", async () => {
3420
+ const savedFetch = globalThis.fetch;
3421
+ globalThis.fetch = async () => new Response(JSON.stringify({ message: "Not found" }), { status: 404 });
3422
+ try {
3423
+ const client = new BudaClient(undefined, "key", "secret");
3424
+ const result = await handleCancelAllOrders(
3425
+ { market_id: "BTC-CLP", confirmation_token: "CONFIRM" },
3426
+ client,
3427
+ );
3428
+ assert(result.isError === true, "should be error");
3429
+ const parsed = JSON.parse(result.content[0].text) as Record<string, unknown>;
3430
+ assert(!("path" in parsed), "error response must NOT contain 'path' field");
3431
+ } finally {
3432
+ globalThis.fetch = savedFetch;
3433
+ }
3434
+ });
3435
+
3436
+ // ----------------------------------------------------------------
3437
+ // Security — validateCurrency in compare_markets
3438
+ // ----------------------------------------------------------------
3439
+
3440
+ section("handleCompareMarkets — validateCurrency guard");
3441
+
3442
+ await test("handleCompareMarkets: base_currency with spaces → INVALID_CURRENCY (no API call)", async () => {
3443
+ let fetchCalled = false;
3444
+ const savedFetch = globalThis.fetch;
3445
+ globalThis.fetch = async () => { fetchCalled = true; return new Response("{}", { status: 200 }); };
3446
+ try {
3447
+ const client = new BudaClient(undefined, "key", "secret");
3448
+ const cache = new MemoryCache();
3449
+ const result = await handleCompareMarkets({ base_currency: "B T C" }, client, cache);
3450
+ const parsed = JSON.parse(result.content[0].text) as { code: string };
3451
+ assertEqual(parsed.code, "INVALID_CURRENCY", "spaces in currency should return INVALID_CURRENCY");
3452
+ assert(result.isError === true, "isError must be true");
3453
+ assert(!fetchCalled, "fetch must NOT be called for invalid currency");
3454
+ } finally {
3455
+ globalThis.fetch = savedFetch;
3456
+ }
3457
+ });
3458
+
3459
+ await test("handleCompareMarkets: base_currency exceeding max length → INVALID_CURRENCY", async () => {
3460
+ const client = new BudaClient(undefined, "key", "secret");
3461
+ const cache = new MemoryCache();
3462
+ const result = await handleCompareMarkets({ base_currency: "A".repeat(20) }, client, cache);
3463
+ const parsed = JSON.parse(result.content[0].text) as { code: string };
3464
+ assertEqual(parsed.code, "INVALID_CURRENCY", "overlong currency should return INVALID_CURRENCY");
3465
+ });
3466
+
3467
+ await test("handleCompareMarkets: empty string → INVALID_CURRENCY", async () => {
3468
+ const client = new BudaClient(undefined, "key", "secret");
3469
+ const cache = new MemoryCache();
3470
+ const result = await handleCompareMarkets({ base_currency: "" }, client, cache);
3471
+ const parsed = JSON.parse(result.content[0].text) as { code: string };
3472
+ assertEqual(parsed.code, "INVALID_CURRENCY", "empty string should return INVALID_CURRENCY");
3473
+ });
3474
+
3475
+ await test("handleCompareMarkets: valid currency passes validation (reaches API/cache)", async () => {
3476
+ const savedFetch = globalThis.fetch;
3477
+ globalThis.fetch = async () => new Response(
3478
+ JSON.stringify({ tickers: [] }),
3479
+ { status: 200 },
3480
+ );
3481
+ try {
3482
+ const client = new BudaClient(undefined, "key", "secret");
3483
+ const cache = new MemoryCache();
3484
+ const result = await handleCompareMarkets({ base_currency: "BTC" }, client, cache);
3485
+ const parsed = JSON.parse(result.content[0].text) as { code?: string };
3486
+ assert(parsed.code !== "INVALID_CURRENCY", "valid currency must not return INVALID_CURRENCY");
3487
+ } finally {
3488
+ globalThis.fetch = savedFetch;
3489
+ }
3490
+ });
3491
+
3492
+ // ----------------------------------------------------------------
3493
+ // Security — BOLT-11 regex improvement
3494
+ // ----------------------------------------------------------------
3495
+
3496
+ section("handleLightningWithdrawal — improved BOLT-11 regex");
3497
+
3498
+ await test("BOLT-11 regex: invoice without bech32 separator '1' → INVALID_INVOICE", async () => {
3499
+ const client = new BudaClient(undefined, "key", "secret");
3500
+ // "lnbc" + 46 chars: passes Zod min(50) but has no '1' separator → should fail new regex
3501
+ const noSeparator = "lnbc" + "a".repeat(46);
3502
+ const result = await handleLightningWithdrawal(
3503
+ { invoice: noSeparator, confirmation_token: "CONFIRM" },
3504
+ client,
3505
+ );
3506
+ const parsed = JSON.parse(result.content[0].text) as { code: string };
3507
+ assertEqual(parsed.code, "INVALID_INVOICE", "invoice without bech32 separator must fail");
3508
+ });
3509
+
3510
+ await test("BOLT-11 regex: invoice with insufficient data after separator → INVALID_INVOICE", async () => {
3511
+ const client = new BudaClient(undefined, "key", "secret");
3512
+ // "lnbc1" + 5 chars (< 20 required after separator) padded to 50 total
3513
+ const tooShortData = "lnbc1" + "a".repeat(5) + "x".repeat(40);
3514
+ // This has '1' at position 5, then only 5 chars of [a-z0-9] before non-bech32 chars
3515
+ // Actually we need to construct this carefully:
3516
+ // New regex: /^ln(bc|tb|bcrt)\d*[munp]?1[a-z0-9]{20,}$/i
3517
+ // "lnbc1aaaaa" has only 5 chars after 1 - fails {20,}
3518
+ const shortAfterSep = "lnbc1" + "a".repeat(5) + " ".repeat(45); // has spaces → fails [a-z0-9] and $
3519
+ const result = await handleLightningWithdrawal(
3520
+ { invoice: shortAfterSep, confirmation_token: "CONFIRM" },
3521
+ client,
3522
+ );
3523
+ const parsed = JSON.parse(result.content[0].text) as { code: string };
3524
+ assertEqual(parsed.code, "INVALID_INVOICE", "invoice with insufficient bech32 data must fail");
3525
+ });
3526
+
3527
+ await test("BOLT-11 regex: valid mainnet invoice structure passes (existing test)", async () => {
3528
+ const savedFetch = globalThis.fetch;
3529
+ globalThis.fetch = async () => new Response(
3530
+ JSON.stringify({ message: "invalid invoice" }), { status: 422 },
3531
+ );
3532
+ try {
3533
+ const client = new BudaClient(undefined, "key", "secret");
3534
+ // Same invoice from existing test — must still pass the format guard
3535
+ const validInvoice = "lntb1230n1pj8ygappp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypq";
3536
+ const result = await handleLightningWithdrawal(
3537
+ { invoice: validInvoice, confirmation_token: "CONFIRM" },
3538
+ client,
3539
+ );
3540
+ const parsed = JSON.parse(result.content[0].text) as { code: string };
3541
+ assert(parsed.code !== "INVALID_INVOICE", "well-formed testnet invoice must pass format guard");
3542
+ } finally {
3543
+ globalThis.fetch = savedFetch;
3544
+ }
3545
+ });
3546
+
3547
+ // ----------------------------------------------------------------
3548
+ // Security — DMS renew/disarm blocked on HTTP transport
3549
+ // ----------------------------------------------------------------
3550
+
3551
+ section("DMS renew_cancel_timer / disarm_cancel_timer — HTTP transport guard");
3552
+
3553
+ await test("renew_cancel_timer: transport='http' returns TRANSPORT_NOT_SUPPORTED", () => {
3554
+ const renewTransportGuard = (transport: "stdio" | "http") => {
3555
+ if (transport === "http") {
3556
+ return Promise.resolve({
3557
+ content: [{ type: "text" as const, text: JSON.stringify({ code: "TRANSPORT_NOT_SUPPORTED" }) }],
3558
+ isError: true,
3559
+ });
3560
+ }
3561
+ return Promise.resolve(handleRenewCancelTimer({ market_id: "BTC-CLP" }, new BudaClient(undefined, "k", "s")));
3562
+ };
3563
+ const result = renewTransportGuard("http");
3564
+ return result.then((r) => {
3565
+ const parsed = JSON.parse(r.content[0].text) as { code: string };
3566
+ assertEqual(parsed.code, "TRANSPORT_NOT_SUPPORTED", "renew on HTTP must return TRANSPORT_NOT_SUPPORTED");
3567
+ assert(r.isError === true, "isError must be true");
3568
+ });
3569
+ });
3570
+
3571
+ await test("disarm_cancel_timer: transport='http' returns TRANSPORT_NOT_SUPPORTED", () => {
3572
+ const disarmTransportGuard = (transport: "stdio" | "http") => {
3573
+ if (transport === "http") {
3574
+ return Promise.resolve({
3575
+ content: [{ type: "text" as const, text: JSON.stringify({ code: "TRANSPORT_NOT_SUPPORTED" }) }],
3576
+ isError: true,
3577
+ });
3578
+ }
3579
+ return Promise.resolve(handleDisarmCancelTimer({ market_id: "BTC-CLP" }));
3580
+ };
3581
+ const result = disarmTransportGuard("http");
3582
+ return result.then((r) => {
3583
+ const parsed = JSON.parse(r.content[0].text) as { code: string };
3584
+ assertEqual(parsed.code, "TRANSPORT_NOT_SUPPORTED", "disarm on HTTP must return TRANSPORT_NOT_SUPPORTED");
3585
+ assert(r.isError === true, "isError must be true");
3586
+ });
3587
+ });
3588
+
3589
+ await test("renew_cancel_timer: transport='stdio' without active timer → NO_ACTIVE_TIMER (handler unchanged)", () => {
3590
+ const client = new BudaClient(undefined, "key", "secret");
3591
+ const result = handleRenewCancelTimer({ market_id: "ETH-CLP" }, client);
3592
+ const parsed = JSON.parse(result.content[0].text) as { code: string };
3593
+ assertEqual(parsed.code, "NO_ACTIVE_TIMER", "no timer should return NO_ACTIVE_TIMER");
3594
+ });
3595
+
3596
+ await test("disarm_cancel_timer: transport='stdio' without active timer → disarmed: false (handler unchanged)", () => {
3597
+ const result = handleDisarmCancelTimer({ market_id: "ETH-CLP" });
3598
+ const parsed = JSON.parse(result.content[0].text) as { disarmed: boolean };
3599
+ assert(parsed.disarmed === false, "disarm with no timer should return disarmed: false");
3600
+ });
3601
+
3602
+ // ----------------------------------------------------------------
3603
+ // Security — safeTokenEqual (constant-time bearer comparison)
3604
+ // ----------------------------------------------------------------
3605
+
3606
+ section("safeTokenEqual — constant-time bearer token comparison");
3607
+
3608
+ await test("safeTokenEqual: identical strings → true", () => {
3609
+ assert(safeTokenEqual("Bearer abc123", "Bearer abc123"), "identical strings must be equal");
3610
+ });
3611
+
3612
+ await test("safeTokenEqual: different same-length strings → false", () => {
3613
+ assert(!safeTokenEqual("Bearer abc", "Bearer xyz"), "different same-length strings must differ");
3614
+ });
3615
+
3616
+ await test("safeTokenEqual: different-length strings → false (no crash)", () => {
3617
+ assert(!safeTokenEqual("short", "longerstring"), "different-length strings must differ");
3618
+ assert(!safeTokenEqual("longerstring", "short"), "reversed different-length must differ");
3619
+ });
3620
+
3621
+ await test("safeTokenEqual: empty strings → true", () => {
3622
+ assert(safeTokenEqual("", ""), "two empty strings are equal");
3623
+ });
3624
+
3625
+ await test("safeTokenEqual: one empty string → false", () => {
3626
+ assert(!safeTokenEqual("", "token"), "empty vs non-empty must differ");
3627
+ assert(!safeTokenEqual("token", ""), "non-empty vs empty must differ");
3628
+ });
3629
+
3630
+ // ----------------------------------------------------------------
3631
+ // Security — parseEnvInt (config validation helper)
3632
+ // ----------------------------------------------------------------
3633
+
3634
+ section("parseEnvInt — environment variable integer validation");
3635
+
3636
+ await test("parseEnvInt: undefined raw → returns fallback", () => {
3637
+ assertEqual(parseEnvInt(undefined, 3000, 1, 65535, "PORT"), 3000, "should return fallback");
3638
+ });
3639
+
3640
+ await test("parseEnvInt: valid string within range → parsed value", () => {
3641
+ assertEqual(parseEnvInt("8080", 3000, 1, 65535, "PORT"), 8080, "should parse 8080");
3642
+ assertEqual(parseEnvInt("120", 120, 1, 10_000, "MCP_RATE_LIMIT"), 120, "should parse 120");
3643
+ });
3644
+
3645
+ await test("parseEnvInt: NaN string → throws", () => {
3646
+ let threw = false;
3647
+ try { parseEnvInt("abc", 3000, 1, 65535, "PORT"); } catch { threw = true; }
3648
+ assert(threw, "non-numeric string should throw");
3649
+ });
3650
+
3651
+ await test("parseEnvInt: value below min → throws", () => {
3652
+ let threw = false;
3653
+ try { parseEnvInt("0", 3000, 1, 65535, "PORT"); } catch { threw = true; }
3654
+ assert(threw, "value 0 below min 1 should throw");
3655
+ });
3656
+
3657
+ await test("parseEnvInt: value above max → throws", () => {
3658
+ let threw = false;
3659
+ try { parseEnvInt("70000", 3000, 1, 65535, "PORT"); } catch { threw = true; }
3660
+ assert(threw, "value 70000 above max 65535 should throw");
3661
+ });
3662
+
3663
+ await test("parseEnvInt: boundary values are accepted", () => {
3664
+ assertEqual(parseEnvInt("1", 3000, 1, 65535, "PORT"), 1, "min boundary should be accepted");
3665
+ assertEqual(parseEnvInt("65535", 3000, 1, 65535, "PORT"), 65535, "max boundary should be accepted");
3666
+ });
3667
+
3067
3668
  // ----------------------------------------------------------------
3068
3669
  // Summary
3069
3670
  // ----------------------------------------------------------------