@yawlabs/lemonsqueezy-mcp 0.10.7 → 0.10.10
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 +50 -1
- package/dist/index.js +46 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,52 @@
|
|
|
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.10] -- 2026-05-22
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **`release.sh` no longer demands a workstation npm session for CI-publish flows.** The pre-flight `npm whoami` gate was a vestige of the local-only-deploy era (added in `53de878`); when CI publishing was restored in `e3ef87a` the gate was kept but the publish itself moved to CI's org-level `NPM_TOKEN`, so the workstation never uses that credential. Blocked the v0.10.10 release attempt with "npm is not authenticated" despite the intended publish path not needing it. Mirrors `tailscale-mcp/release.sh`, which never had the gate.
|
|
10
|
+
|
|
11
|
+
### Security
|
|
12
|
+
|
|
13
|
+
- **Webhook `url` restricted to http/https schemes.** `ls_create_webhook` and `ls_update_webhook` previously accepted any string up to 10k chars; `z.string().url()` alone would have additionally accepted every URL-parseable scheme (`mailto:`, `file:`, `javascript:`, `ftp:`, `chrome-extension:`, ...). Anything non-http(s) stored as a webhook target is unreachable in the best case and an injection sink in the worst. The new `httpsUrlSchema` chains `.url()` with a `.refine()` enforcing `^https?://`, applied on both tools.
|
|
14
|
+
- **`ls_update_webhook` secret rotation gated as destructive.** Rotating the signing secret silently breaks signature verification on every receiver until they redeploy with the new value. The tool now declares an `isDestructive` predicate flagging any `secret` change, routing those calls through `checkDestructiveRateLimit` + audit logging via the wrapper's existing duck-typed integration. URL and events changes stay on the regular path.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- **`ls_update_webhook` rejects empty PATCH locally.** Calling with only `webhookId` and no fields to change previously sent an empty `attributes: {}` PATCH that the API would 422 with a less clear message. The handler now throws `"ls_update_webhook requires at least one of: url, events, secret"` before the round-trip.
|
|
19
|
+
- **`ls_update_webhook` description warns about secret rotation.** The tool-level description now calls out that setting `secret` is destructive (rate-limited and audited) so LLM callers see the consequence before invoking.
|
|
20
|
+
|
|
21
|
+
### Tests
|
|
22
|
+
|
|
23
|
+
- **Regression coverage for the `events.min(1)` fix** (`f7374bd`): schema-rejection tests on both `ls_create_webhook` and `ls_update_webhook` lock in the empty-array rejection.
|
|
24
|
+
- **URL validation coverage**: non-URL string rejection on both tools, four non-http(s) schemes (`mailto:`, `file:`, `javascript:`, `ftp:`) rejected on both tools, http and https explicit acceptance.
|
|
25
|
+
- **`ls_update_webhook` predicate tests** mirror the `ls_update_license_key` shape: secret-only as destructive, url-only and events-only as non-destructive, empty input as non-destructive.
|
|
26
|
+
- **`ls_update_webhook` URL-encoding** test added in parallel to the existing `ls_delete_webhook` coverage.
|
|
27
|
+
- **Handler-side empty-PATCH rejection** test asserts the new error message.
|
|
28
|
+
|
|
29
|
+
## [0.10.9] -- 2026-05-22
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- **Webhook `events` array now requires at least one entry.** `ls_create_webhook` and `ls_update_webhook` previously let an empty array pass local Zod validation, so a no-op webhook configuration only failed at the LemonSqueezy API as a 422. The new `.min(1)` makes the rejection local with a clearer message; on update, the field stays optional but, when supplied, must be non-empty.
|
|
34
|
+
|
|
35
|
+
## [0.10.8] -- 2026-05-19
|
|
36
|
+
|
|
37
|
+
### Security
|
|
38
|
+
|
|
39
|
+
- **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`.
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
|
|
43
|
+
- **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.
|
|
44
|
+
- **`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.
|
|
45
|
+
|
|
46
|
+
### Tests
|
|
47
|
+
|
|
48
|
+
- **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.
|
|
49
|
+
- **`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.
|
|
50
|
+
|
|
5
51
|
## [0.10.7] -- 2026-05-16
|
|
6
52
|
|
|
7
53
|
### Security
|
|
@@ -343,7 +389,10 @@ Hardening pass for unattended automation against live billing flows.
|
|
|
343
389
|
|
|
344
390
|
Initial release. 59 tools covering all 17 LemonSqueezy API resources.
|
|
345
391
|
|
|
346
|
-
[Unreleased]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.
|
|
392
|
+
[Unreleased]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.9...HEAD
|
|
393
|
+
[0.10.10]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.9...v0.10.10
|
|
394
|
+
[0.10.9]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.8...v0.10.9
|
|
395
|
+
[0.10.8]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.7...v0.10.8
|
|
347
396
|
[0.10.7]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.6...v0.10.7
|
|
348
397
|
[0.10.6]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.5...v0.10.6
|
|
349
398
|
[0.10.5]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.4...v0.10.5
|
package/dist/index.js
CHANGED
|
@@ -30653,10 +30653,7 @@ async function loadApiKey() {
|
|
|
30653
30653
|
if (testRaw && testRaw.trim() !== "") {
|
|
30654
30654
|
const fingerprint2 = fingerprintFor("test", testRaw);
|
|
30655
30655
|
const hit2 = fromCache(fingerprint2);
|
|
30656
|
-
if (hit2 !== null)
|
|
30657
|
-
announceTestModeOnce();
|
|
30658
|
-
return hit2;
|
|
30659
|
-
}
|
|
30656
|
+
if (hit2 !== null) return hit2;
|
|
30660
30657
|
announceTestModeOnce();
|
|
30661
30658
|
intoCache(fingerprint2, testRaw);
|
|
30662
30659
|
return testRaw;
|
|
@@ -31863,6 +31860,16 @@ function loadSinkConfig() {
|
|
|
31863
31860
|
function isToolHandlerResponse(value) {
|
|
31864
31861
|
return "ok" in value;
|
|
31865
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
|
+
}
|
|
31866
31873
|
function buildSinkPath(path, params) {
|
|
31867
31874
|
const parts = [];
|
|
31868
31875
|
for (const [k, v] of Object.entries(params)) {
|
|
@@ -31897,7 +31904,12 @@ async function sinkRequest(config2, method, pathAndQuery) {
|
|
|
31897
31904
|
if (!res.ok) {
|
|
31898
31905
|
let body = "";
|
|
31899
31906
|
try {
|
|
31900
|
-
|
|
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
|
+
}
|
|
31901
31913
|
} catch {
|
|
31902
31914
|
}
|
|
31903
31915
|
let detail = body;
|
|
@@ -31923,7 +31935,20 @@ async function sinkRequest(config2, method, pathAndQuery) {
|
|
|
31923
31935
|
error: `Sink returned ${res.status}: ${detail || res.statusText}`
|
|
31924
31936
|
};
|
|
31925
31937
|
}
|
|
31926
|
-
|
|
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;
|
|
31927
31952
|
if (!text.trim()) return { ok: true, data: {} };
|
|
31928
31953
|
if (text.length > MAX_BODY_SIZE_BYTES) {
|
|
31929
31954
|
return {
|
|
@@ -32516,6 +32541,7 @@ var variantTools = [
|
|
|
32516
32541
|
];
|
|
32517
32542
|
|
|
32518
32543
|
// src/tools/webhooks.ts
|
|
32544
|
+
var httpsUrlSchema = external_exports3.string().url().max(1e4).refine((u) => /^https?:\/\//i.test(u), { message: "url must start with http:// or https://" });
|
|
32519
32545
|
var webhookTools = [
|
|
32520
32546
|
{
|
|
32521
32547
|
name: "ls_get_webhook",
|
|
@@ -32566,8 +32592,8 @@ var webhookTools = [
|
|
|
32566
32592
|
},
|
|
32567
32593
|
inputSchema: external_exports3.object({
|
|
32568
32594
|
storeId: lsIdSchema.describe("The store ID"),
|
|
32569
|
-
url:
|
|
32570
|
-
events: external_exports3.array(external_exports3.string()).describe(
|
|
32595
|
+
url: httpsUrlSchema.describe("The URL to send webhook events to (must be a valid http/https URL)"),
|
|
32596
|
+
events: external_exports3.array(external_exports3.string()).min(1).describe(
|
|
32571
32597
|
"Event types to subscribe to (e.g. ['order_created', 'subscription_created', 'subscription_updated', 'subscription_cancelled', 'subscription_payment_success', 'subscription_payment_failed', 'license_key_created'])"
|
|
32572
32598
|
),
|
|
32573
32599
|
secret: external_exports3.string().min(6).max(40).describe("A signing secret for verifying webhook payloads")
|
|
@@ -32591,7 +32617,7 @@ var webhookTools = [
|
|
|
32591
32617
|
{
|
|
32592
32618
|
name: "ls_update_webhook",
|
|
32593
32619
|
authorityClass: "webhook",
|
|
32594
|
-
description: "Update an existing webhook's URL, events, or secret.",
|
|
32620
|
+
description: "Update an existing webhook's URL, events, or secret. Setting `secret` rotates the signing secret and breaks signature verification on the receiver until they update their copy -- this is treated as destructive (rate-limited and audited).",
|
|
32595
32621
|
annotations: {
|
|
32596
32622
|
title: "Update webhook",
|
|
32597
32623
|
readOnlyHint: false,
|
|
@@ -32599,10 +32625,15 @@ var webhookTools = [
|
|
|
32599
32625
|
idempotentHint: true,
|
|
32600
32626
|
openWorldHint: true
|
|
32601
32627
|
},
|
|
32628
|
+
// Rotating the signing secret silently breaks signature verification on
|
|
32629
|
+
// every receiver until they redeploy with the new value. URL and events
|
|
32630
|
+
// changes are observable on the next event but don't break anything in
|
|
32631
|
+
// flight, so they stay on the regular path.
|
|
32632
|
+
isDestructive: (input) => input.secret !== void 0,
|
|
32602
32633
|
inputSchema: external_exports3.object({
|
|
32603
32634
|
webhookId: lsIdSchema.describe("The webhook ID to update"),
|
|
32604
|
-
url:
|
|
32605
|
-
events: external_exports3.array(external_exports3.string()).optional().describe("Updated list of event types to subscribe to"),
|
|
32635
|
+
url: httpsUrlSchema.optional().describe("New URL to send webhook events to (must be a valid http/https URL)"),
|
|
32636
|
+
events: external_exports3.array(external_exports3.string()).min(1).optional().describe("Updated list of event types to subscribe to"),
|
|
32606
32637
|
secret: external_exports3.string().min(6).max(40).optional().describe("New signing secret")
|
|
32607
32638
|
}),
|
|
32608
32639
|
handler: async (input) => {
|
|
@@ -32610,6 +32641,9 @@ var webhookTools = [
|
|
|
32610
32641
|
if (input.url !== void 0) attributes.url = input.url;
|
|
32611
32642
|
if (input.events !== void 0) attributes.events = input.events;
|
|
32612
32643
|
if (input.secret !== void 0) attributes.secret = input.secret;
|
|
32644
|
+
if (Object.keys(attributes).length === 0) {
|
|
32645
|
+
throw new Error("ls_update_webhook requires at least one of: url, events, secret");
|
|
32646
|
+
}
|
|
32613
32647
|
return apiPatch(`/webhooks/${encodePath(input.webhookId)}`, {
|
|
32614
32648
|
data: {
|
|
32615
32649
|
type: "webhooks",
|
|
@@ -32806,7 +32840,7 @@ function readAuditLogResource(uri) {
|
|
|
32806
32840
|
}
|
|
32807
32841
|
|
|
32808
32842
|
// src/index.ts
|
|
32809
|
-
var version2 = true ? "0.10.
|
|
32843
|
+
var version2 = true ? "0.10.10" : (await null).createRequire(import.meta.url)("../package.json").version;
|
|
32810
32844
|
var subcommand = process.argv[2];
|
|
32811
32845
|
if (subcommand === "version" || subcommand === "--version") {
|
|
32812
32846
|
console.log(version2);
|
package/package.json
CHANGED