@yawlabs/lemonsqueezy-mcp 0.10.6 → 0.10.8

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 CHANGED
@@ -2,6 +2,32 @@
2
2
 
3
3
  All notable changes to `@yawlabs/lemonsqueezy-mcp` are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and versioning follows [SEMVER.md](./SEMVER.md).
4
4
 
5
+ ## [0.10.8] -- 2026-05-19
6
+
7
+ ### Security
8
+
9
+ - **Sink response body size guard extended to the error branch.** `sinkRequest` in `src/tools/sink.ts` previously only size-checked 2xx response bodies; a misbehaving sink returning a giant 4xx/5xx body could still buffer the whole thing into memory before any limit fired. A new `readBodyOrSizeError` helper pre-checks `Content-Length` against the 10 MB cap and is applied to both the error and success branches. The post-read length check on the 2xx path is retained as belt-and-braces against a lying `Content-Length`.
10
+
11
+ ### Changed
12
+
13
+ - **2xx body-read mid-stream failures now collapse to the uniform `{ ok: false, error }` shape.** Previously a socket reset partway through reading the body propagated as an exception out of `sinkRequest`, surfacing as a less-informative error via the wrapper's catch-all. The 2xx body read is now wrapped in a try/catch that returns `Sink response body read failed: <message>` -- consistent with every other failure mode in the function.
14
+ - **`src/secret.ts` cache-hit branch tightened.** The test-mode cache-hit branch previously called `announceTestModeOnce()` redundantly (the flag is set on the first miss, so the cache-hit call was dead). Removed the call and the now-obsolete defensive comment.
15
+
16
+ ### Tests
17
+
18
+ - **Sink coverage expanded** to pin `authorityClass` per tool (`read` for `ls_sink_events_list` / `ls_sink_stats`, `mutate` for `ls_sink_event_mark_processed`), 4xx-oversized-Content-Length, 2xx-lying-Content-Length, and 2xx mid-stream body-read failures. `stubFetch` gained an optional `responseHeaders` field so error-branch tests no longer override `globalThis.fetch` inline.
19
+ - **`parseCommand` contract pinned** in `src/secret.test.ts` for the four tokenizer edges that the rest of the suite only touched by accident: unterminated quote, all-quotes-collapse-to-empty, quoted-args-with-spaces (both quote styles), and quote-then-bare-word concatenation. Also added a 64 KB `maxBuffer` overflow test that exercises the `execFile` `ERR_CHILD_PROCESS_STDIO_MAXBUFFER` rewrap path.
20
+
21
+ ## [0.10.7] -- 2026-05-16
22
+
23
+ ### Security
24
+
25
+ - **Allowlist-bypass surface closed at the test layer.** `checkStoreScopedToolInput` in `src/wrapper.ts` enforces `LEMONSQUEEZY_ALLOWED_STORE_IDS` by reading the literal input field name `storeId` from the tool's Zod shape. Today every list tool that filters by store happens to use that name, but the convention wasn't asserted anywhere -- a future tool whose filterMap mapped a differently-named input (e.g. `store`) to `filter[store_id]` would silently bypass the allowlist. `src/api.ts` `listHandler` now exposes its `filterMap` on the returned handler, and `src/tools/tools.test.ts` adds an invariant that fails CI for any drift between the filter key (`store_id`) and the input field name (`storeId`).
26
+
27
+ ### Changed
28
+
29
+ - **`wrapper.ts` skips the success/error entry literal when no consumer wants it.** A non-destructive read at `LEMONSQUEEZY_LOG` unset (i.e. "off") previously built a full audit entry on every call, then `logEvent` discarded it inside its level check. Now the wrapper consults `wouldLogToolCall({ isDestructive, isError })` -- a new export from `src/logger.ts` -- before allocating. Destructive calls always build the entry because the audit-buffer push is independent of the log level. No observable behavior change; saves a small object literal per non-destructive read on the most common configuration.
30
+
5
31
  ## [0.10.6] -- 2026-05-16
6
32
 
7
33
  ### Security
@@ -333,7 +359,9 @@ Hardening pass for unattended automation against live billing flows.
333
359
 
334
360
  Initial release. 59 tools covering all 17 LemonSqueezy API resources.
335
361
 
336
- [Unreleased]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.6...HEAD
362
+ [Unreleased]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.8...HEAD
363
+ [0.10.8]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.7...v0.10.8
364
+ [0.10.7]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.6...v0.10.7
337
365
  [0.10.6]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.5...v0.10.6
338
366
  [0.10.5]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.4...v0.10.5
339
367
  [0.10.4]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.3...v0.10.4
package/dist/index.js CHANGED
@@ -30410,6 +30410,19 @@ function shouldLog(entry, level) {
30410
30410
  return isErrorEntry(entry);
30411
30411
  }
30412
30412
  }
30413
+ function wouldLogToolCall(opts) {
30414
+ const level = getLogLevel();
30415
+ switch (level) {
30416
+ case "off":
30417
+ return false;
30418
+ case "all":
30419
+ return true;
30420
+ case "audit":
30421
+ return opts.isDestructive || opts.isError;
30422
+ case "error":
30423
+ return opts.isError;
30424
+ }
30425
+ }
30413
30426
  function safeString(value) {
30414
30427
  if (value === void 0) return void 0;
30415
30428
  if (typeof value === "string") return value;
@@ -30640,10 +30653,7 @@ async function loadApiKey() {
30640
30653
  if (testRaw && testRaw.trim() !== "") {
30641
30654
  const fingerprint2 = fingerprintFor("test", testRaw);
30642
30655
  const hit2 = fromCache(fingerprint2);
30643
- if (hit2 !== null) {
30644
- announceTestModeOnce();
30645
- return hit2;
30646
- }
30656
+ if (hit2 !== null) return hit2;
30647
30657
  announceTestModeOnce();
30648
30658
  intoCache(fingerprint2, testRaw);
30649
30659
  return testRaw;
@@ -30886,7 +30896,7 @@ function getHandler(endpoint, idField) {
30886
30896
  };
30887
30897
  }
30888
30898
  function listHandler(endpoint, filterMap = {}) {
30889
- return async (input) => {
30899
+ const handler = async (input) => {
30890
30900
  const filter = {};
30891
30901
  for (const [inputKey, apiKey] of Object.entries(filterMap)) {
30892
30902
  const val = input[inputKey];
@@ -30899,6 +30909,7 @@ function listHandler(endpoint, filterMap = {}) {
30899
30909
  });
30900
30910
  return apiGet(`${endpoint}${query}`);
30901
30911
  };
30912
+ return Object.assign(handler, { filterMap });
30902
30913
  }
30903
30914
  async function apiGet(path) {
30904
30915
  return apiRequest("GET", path);
@@ -31849,6 +31860,16 @@ function loadSinkConfig() {
31849
31860
  function isToolHandlerResponse(value) {
31850
31861
  return "ok" in value;
31851
31862
  }
31863
+ async function readBodyOrSizeError(res) {
31864
+ const contentLength = res.headers.get("content-length");
31865
+ if (contentLength) {
31866
+ const declared = Number.parseInt(contentLength, 10);
31867
+ if (Number.isFinite(declared) && declared > MAX_BODY_SIZE_BYTES) {
31868
+ return { ok: false, declared };
31869
+ }
31870
+ }
31871
+ return { ok: true, text: await res.text() };
31872
+ }
31852
31873
  function buildSinkPath(path, params) {
31853
31874
  const parts = [];
31854
31875
  for (const [k, v] of Object.entries(params)) {
@@ -31883,7 +31904,12 @@ async function sinkRequest(config2, method, pathAndQuery) {
31883
31904
  if (!res.ok) {
31884
31905
  let body = "";
31885
31906
  try {
31886
- body = await res.text();
31907
+ const readResult2 = await readBodyOrSizeError(res);
31908
+ if (readResult2.ok) {
31909
+ body = readResult2.text;
31910
+ } else {
31911
+ body = `[body too large: ${readResult2.declared} bytes exceeds ${MAX_BODY_SIZE_BYTES} byte limit]`;
31912
+ }
31887
31913
  } catch {
31888
31914
  }
31889
31915
  let detail = body;
@@ -31909,7 +31935,20 @@ async function sinkRequest(config2, method, pathAndQuery) {
31909
31935
  error: `Sink returned ${res.status}: ${detail || res.statusText}`
31910
31936
  };
31911
31937
  }
31912
- const text = await res.text();
31938
+ let readResult;
31939
+ try {
31940
+ readResult = await readBodyOrSizeError(res);
31941
+ } catch (err) {
31942
+ const message = err instanceof Error ? err.message : String(err);
31943
+ return { ok: false, error: `Sink response body read failed: ${message}` };
31944
+ }
31945
+ if (!readResult.ok) {
31946
+ return {
31947
+ ok: false,
31948
+ error: `Sink response body too large: ${readResult.declared} bytes exceeds ${MAX_BODY_SIZE_BYTES} byte limit`
31949
+ };
31950
+ }
31951
+ const text = readResult.text;
31913
31952
  if (!text.trim()) return { ok: true, data: {} };
31914
31953
  if (text.length > MAX_BODY_SIZE_BYTES) {
31915
31954
  return {
@@ -32719,19 +32758,22 @@ function createToolHandler(tool) {
32719
32758
  checkStoreScopedToolInput(toolAsRecord, inputAsRecord);
32720
32759
  const response = await tool.handler(input);
32721
32760
  const latency_ms = Date.now() - start;
32722
- const successEntry = {
32723
- event: "tool_call",
32724
- tool: tool.name,
32725
- status: response.ok ? "ok" : "error",
32726
- latency_ms,
32727
- request_id: response.requestId,
32728
- error: response.ok ? void 0 : response.error,
32729
- audit: isDestructive ? true : void 0,
32730
- inputs: isDestructive ? redactSecrets(input) : void 0
32731
- };
32732
- logEvent(successEntry);
32733
- if (isDestructive) {
32734
- pushAuditEntry({ ts: (/* @__PURE__ */ new Date()).toISOString(), ...successEntry });
32761
+ const wouldLog = wouldLogToolCall({ isDestructive, isError: !response.ok });
32762
+ if (wouldLog || isDestructive) {
32763
+ const successEntry = {
32764
+ event: "tool_call",
32765
+ tool: tool.name,
32766
+ status: response.ok ? "ok" : "error",
32767
+ latency_ms,
32768
+ request_id: response.requestId,
32769
+ error: response.ok ? void 0 : response.error,
32770
+ audit: isDestructive ? true : void 0,
32771
+ inputs: isDestructive ? redactSecrets(input) : void 0
32772
+ };
32773
+ if (wouldLog) logEvent(successEntry);
32774
+ if (isDestructive) {
32775
+ pushAuditEntry({ ts: (/* @__PURE__ */ new Date()).toISOString(), ...successEntry });
32776
+ }
32735
32777
  }
32736
32778
  if (!response.ok) {
32737
32779
  return {
@@ -32751,18 +32793,21 @@ function createToolHandler(tool) {
32751
32793
  } catch (err) {
32752
32794
  const message = err instanceof Error ? err.message : String(err);
32753
32795
  const latency_ms = Date.now() - start;
32754
- const errorEntry = {
32755
- event: "tool_call",
32756
- tool: tool.name,
32757
- status: err instanceof GuardrailError ? "guardrail_block" : "exception",
32758
- latency_ms,
32759
- error: message,
32760
- audit: isDestructive ? true : void 0,
32761
- inputs: isDestructive ? redactSecrets(input) : void 0
32762
- };
32763
- logEvent(errorEntry);
32764
- if (isDestructive) {
32765
- pushAuditEntry({ ts: (/* @__PURE__ */ new Date()).toISOString(), ...errorEntry });
32796
+ const wouldLog = wouldLogToolCall({ isDestructive, isError: true });
32797
+ if (wouldLog || isDestructive) {
32798
+ const errorEntry = {
32799
+ event: "tool_call",
32800
+ tool: tool.name,
32801
+ status: err instanceof GuardrailError ? "guardrail_block" : "exception",
32802
+ latency_ms,
32803
+ error: message,
32804
+ audit: isDestructive ? true : void 0,
32805
+ inputs: isDestructive ? redactSecrets(input) : void 0
32806
+ };
32807
+ if (wouldLog) logEvent(errorEntry);
32808
+ if (isDestructive) {
32809
+ pushAuditEntry({ ts: (/* @__PURE__ */ new Date()).toISOString(), ...errorEntry });
32810
+ }
32766
32811
  }
32767
32812
  return {
32768
32813
  content: [{ type: "text", text: `Error: ${message}` }],
@@ -32786,7 +32831,7 @@ function readAuditLogResource(uri) {
32786
32831
  }
32787
32832
 
32788
32833
  // src/index.ts
32789
- var version2 = true ? "0.10.6" : (await null).createRequire(import.meta.url)("../package.json").version;
32834
+ var version2 = true ? "0.10.8" : (await null).createRequire(import.meta.url)("../package.json").version;
32790
32835
  var subcommand = process.argv[2];
32791
32836
  if (subcommand === "version" || subcommand === "--version") {
32792
32837
  console.log(version2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yawlabs/lemonsqueezy-mcp",
3
- "version": "0.10.6",
3
+ "version": "0.10.8",
4
4
  "mcpName": "io.github.YawLabs/lemonsqueezy-mcp",
5
5
  "description": "LemonSqueezy MCP server for managing your store from AI assistants",
6
6
  "license": "MIT",