@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 +29 -1
- package/dist/index.js +78 -33
- package/package.json +1 -1
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
32723
|
-
|
|
32724
|
-
|
|
32725
|
-
|
|
32726
|
-
|
|
32727
|
-
|
|
32728
|
-
|
|
32729
|
-
|
|
32730
|
-
|
|
32731
|
-
|
|
32732
|
-
|
|
32733
|
-
|
|
32734
|
-
|
|
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
|
|
32755
|
-
|
|
32756
|
-
|
|
32757
|
-
|
|
32758
|
-
|
|
32759
|
-
|
|
32760
|
-
|
|
32761
|
-
|
|
32762
|
-
|
|
32763
|
-
|
|
32764
|
-
|
|
32765
|
-
|
|
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.
|
|
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