@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 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.7...HEAD
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
- 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
+ }
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
- 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;
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: external_exports3.string().max(1e4).describe("The URL to send webhook events to"),
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: external_exports3.string().max(1e4).optional().describe("New URL to send webhook events to"),
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.7" : (await null).createRequire(import.meta.url)("../package.json").version;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yawlabs/lemonsqueezy-mcp",
3
- "version": "0.10.7",
3
+ "version": "0.10.10",
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",