@yawlabs/lemonsqueezy-mcp 0.10.5 → 0.10.7

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 (3) hide show
  1. package/CHANGELOG.md +75 -48
  2. package/dist/index.js +56 -29
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
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.7] -- 2026-05-16
6
+
7
+ ### Security
8
+
9
+ - **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`).
10
+
11
+ ### Changed
12
+
13
+ - **`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.
14
+
15
+ ## [0.10.6] -- 2026-05-16
16
+
17
+ ### Security
18
+
19
+ - **Sink response body size cap.** `sinkRequest` in `src/tools/sink.ts` now rejects responses larger than 10 MB before calling `JSON.parse`, defending the in-process MCP server against CPU exhaustion from a malicious or misconfigured sink (`Sink response body too large: X bytes exceeds Y byte limit`). The 10 s `AbortSignal` timeout already bounded wall-clock; the size cap bounds the parse cost as well.
20
+
21
+ ### Changed
22
+
23
+ - **`buildQuery` page-param values are URL-encoded.** Brought `page[number]` / `page[size]` in line with the `include` / `filter` branches (which already wrapped values in `encodeURIComponent`). Today the Zod schemas constrain both to integers so the encoding is a no-op, but if pagination ever widens to accept a string cursor the values are already safely escaped.
24
+ - **`smithery.yaml` numeric fields are typed `integer`.** `lemonsqueezyMaxRefundAmountCents` and `lemonsqueezyDestructiveRateLimit` now declare `type: integer, minimum: 1` (were `type: string`). Smithery's config UI now renders a number input with client-side validation. The `commandFunction` `String(...)` coerces back to env-var form, and the truthy check switched to `!= null` so the (admittedly nonsensical) value `0` would not be silently dropped.
25
+
26
+ ### Docs
27
+
28
+ - **`CHANGELOG.md` em-dashes normalized to ASCII `--`** throughout v0.9.3 and earlier sections. Brings older entries in line with the v0.10.x style and avoids the Windows-terminal mojibake risk called out in the Yaw Mode discipline.
29
+
5
30
  ## [0.10.5] -- 2026-05-16
6
31
 
7
32
  ### Changed
@@ -48,17 +73,17 @@ All notable changes to `@yawlabs/lemonsqueezy-mcp` are documented here. The form
48
73
  - `ls_sink_stats` -- total, unprocessed, last-received timestamp
49
74
  - `LEMONSQUEEZY_SINK_URL` and `LEMONSQUEEZY_SINK_ADMIN_TOKEN` env vars to configure the bridge. Tools are always registered for `tools/list` discovery; missing env vars surface at call time as a structured "not configured" error (with a pointer to the sink repo), not a registration-time failure. 10s fetch timeout via `AbortSignal.timeout()`; auth failures (401), admin-disabled (404), and transport errors each get a tailored diagnostic.
50
75
 
51
- ## [0.9.3] 2026-05-15
76
+ ## [0.9.3] -- 2026-05-15
52
77
 
53
78
  ### Fixed
54
79
 
55
80
  - `server.json` rewritten against the current registry schema (`2025-12-11`). The 0.9.2 release's MCP-Registry publish step failed validation against the live schema: `description` exceeded the 100-char cap, the package field was `registry_type` (snake_case) where the schema expects `registryType` (camelCase), and the required `packages[].transport` field was missing. 0.9.2 is live on npm and GitHub but did not reach the Official MCP Registry. 0.9.3 is the first release that should land on `registry.modelcontextprotocol.io`.
56
81
 
57
- ## [0.9.2] 2026-05-15
82
+ ## [0.9.2] -- 2026-05-15
58
83
 
59
84
  ### Added
60
85
 
61
- - `.github/workflows/release.yml` now publishes to the [Official MCP Registry](https://registry.modelcontextprotocol.io) after the post-publish smoke test passes. Authentication is via GitHub OIDC the `id-token: write` permission already enabled for npm provenance also satisfies the registry's auth; no `MCP_*` secret is required. The namespace `io.github.YawLabs/*` is authorized purely from the OIDC `repository_owner` claim, so anyone outside the YawLabs GitHub org cannot publish under it. A `jq` step overwrites `server.json`'s `version` (and `packages[0].version`) from the pushed tag, so a forgotten manual bump on `server.json` no longer publishes a stale version to the registry. Downstream registries (Glama, PulseMCP, mcpservers.org) that auto-source from the official registry now pick up each `@yawlabs/lemonsqueezy-mcp` release without a manual mcp-publisher run.
86
+ - `.github/workflows/release.yml` now publishes to the [Official MCP Registry](https://registry.modelcontextprotocol.io) after the post-publish smoke test passes. Authentication is via GitHub OIDC -- the `id-token: write` permission already enabled for npm provenance also satisfies the registry's auth; no `MCP_*` secret is required. The namespace `io.github.YawLabs/*` is authorized purely from the OIDC `repository_owner` claim, so anyone outside the YawLabs GitHub org cannot publish under it. A `jq` step overwrites `server.json`'s `version` (and `packages[0].version`) from the pushed tag, so a forgotten manual bump on `server.json` no longer publishes a stale version to the registry. Downstream registries (Glama, PulseMCP, mcpservers.org) that auto-source from the official registry now pick up each `@yawlabs/lemonsqueezy-mcp` release without a manual mcp-publisher run.
62
87
 
63
88
  ### Docs
64
89
 
@@ -66,24 +91,24 @@ All notable changes to `@yawlabs/lemonsqueezy-mcp` are documented here. The form
66
91
  - README Development section documents `npm run gen:containerfile` / `npm run check:containerfile` so a contributor editing `Dockerfile` knows to regenerate.
67
92
  - README Releasing section notes that the local `./release.sh` path does not publish to the Official MCP Registry (CI-only) and gives the manual `mcp-publisher` commands as the fallback when a release is cut without CI.
68
93
 
69
- ## [0.9.1] 2026-05-15
94
+ ## [0.9.1] -- 2026-05-15
70
95
 
71
96
  ### Docs
72
97
 
73
- - README "Features" list now mentions authority-class disable, per-class rate limits, and the audit-log MCP Resource previously omitted under the umbrella "Guardrails" bullet despite being documented in detail later in the file.
98
+ - README "Features" list now mentions authority-class disable, per-class rate limits, and the audit-log MCP Resource -- previously omitted under the umbrella "Guardrails" bullet despite being documented in detail later in the file.
74
99
  - README description of `LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT` now lists every destructive trigger the runtime classifier counts: `ls_update_license_key` calls that change `activationLimit` (added in 0.7.1), `ls_update_customer` calls with `status: "archived"`, plus the previously-documented pause/plan-switch and `disabled: true` paths.
75
100
  - README qualifies the "issue a scoped LemonSqueezy API key" recommendation. LS API keys are account-wide; the qualifier directs readers to LS team-membership scoping where it exists and treats the env-var guardrails as the primary control surface for what LS itself can't express (per-class rate ceilings).
76
101
 
77
102
  ### Changed
78
103
 
79
104
  - `audit-buffer.ts` overflow path is now O(1) (circular index) instead of O(N) (`Array.splice(0, …)`). No observable behavior change; the buffer caps at 1000 entries and presents the same most-recent-first read order.
80
- - `src/index.ts` split the registered MCP tool wrapper and the audit-log Resource read-callback now live in `src/wrapper.ts` so unit tests can exercise them end-to-end without starting the stdio MCP server. `src/index.ts` becomes a thin entry point that imports `createToolHandler` and `readAuditLogResource` from the new module. No observable behavior change in the published binary.
105
+ - `src/index.ts` split -- the registered MCP tool wrapper and the audit-log Resource read-callback now live in `src/wrapper.ts` so unit tests can exercise them end-to-end without starting the stdio MCP server. `src/index.ts` becomes a thin entry point that imports `createToolHandler` and `readAuditLogResource` from the new module. No observable behavior change in the published binary.
81
106
  - `secret.ts` cache fingerprint no longer embeds the raw API key. The fingerprint is now a SHA-256 digest of `mode:value`, preserving the mid-process change-detection semantics while removing the only in-memory copy of the key outside the `cached.key` field itself.
82
107
  - `redact.ts` now also masks any string value matching the LemonSqueezy bearer-token shape (`eyJ…`-style JWT prefix), even when the key name is innocuous. Closes the future-feature gap where a destructive tool accepts a `customData`/`metadata` object whose values happen to contain a key. Also covers additional credential / PII key names (`private_key`, `pin`, `ssn`, `social_security_number`, `credit_card`, `card_number`, `cvv`, `cvc`).
83
108
 
84
109
  ### Fixed
85
110
 
86
- - Smithery one-click install now exposes `LEMONSQUEEZY_DISABLE_CLASSES` and `LEMONSQUEEZY_RATE_LIMIT_PER_CLASS` the two env vars added in 0.9.0. Previously a Smithery user had to hand-edit the spawned process env to reach the authority-class layer.
111
+ - Smithery one-click install now exposes `LEMONSQUEEZY_DISABLE_CLASSES` and `LEMONSQUEEZY_RATE_LIMIT_PER_CLASS` -- the two env vars added in 0.9.0. Previously a Smithery user had to hand-edit the spawned process env to reach the authority-class layer.
87
112
  - Smithery description for `lemonsqueezyDestructiveRateLimit` now states the actual format (a positive integer interpreted as calls per 60-second rolling window) instead of "Format depends on server defaults."
88
113
  - `logger.ts` JSON-stringify fallback now coerces `entry.error` to a plain string before the second serialization pass, so a non-string error field (an `Error` with a circular `cause` chain, or an object with a throwing `toJSON`) no longer skips both write attempts and degrades to silence. Tests cover the case.
89
114
  - `integration.test.ts` `before()` hook now clears `LEMONSQUEEZY_API_KEY_COMMAND` and `LEMONSQUEEZY_TEST_API_KEY` before setting `LEMONSQUEEZY_API_KEY` from the test key, and resets the secret cache. Previously a developer with a vault-backed `LEMONSQUEEZY_API_KEY_COMMAND` would silently run the integration suite against their dev key, because the command takes precedence over the bare env var in `secret.ts`.
@@ -91,54 +116,54 @@ All notable changes to `@yawlabs/lemonsqueezy-mcp` are documented here. The form
91
116
 
92
117
  ### Added
93
118
 
94
- - `src/wrapper.ts` extracted production module containing `createToolHandler` (the MCP tool registration wrapper) and `readAuditLogResource` (the audit-log Resource read-callback). Same logic that previously lived inline in `src/index.ts`; now reusable and testable.
95
- - `src/wrapper.test.ts` end-to-end test that exercises the registered MCP tool wrapper from `src/wrapper.ts`. Covers the order in which `checkClassAllowed` / `checkClassRateLimit` / `checkDestructiveRateLimit` / `checkStoreScopedToolInput` fire, the destructive-call audit pipeline (logger + ring buffer), the audit-log MCP Resource (`lemonsqueezy://audit-log`), and the predicate-destructive routing (e.g. `ls_update_license_key` with `activationLimit`). Previously every handler test invoked `tool.handler(input)` directly and the wrapper was untested.
119
+ - `src/wrapper.ts` -- extracted production module containing `createToolHandler` (the MCP tool registration wrapper) and `readAuditLogResource` (the audit-log Resource read-callback). Same logic that previously lived inline in `src/index.ts`; now reusable and testable.
120
+ - `src/wrapper.test.ts` -- end-to-end test that exercises the registered MCP tool wrapper from `src/wrapper.ts`. Covers the order in which `checkClassAllowed` / `checkClassRateLimit` / `checkDestructiveRateLimit` / `checkStoreScopedToolInput` fire, the destructive-call audit pipeline (logger + ring buffer), the audit-log MCP Resource (`lemonsqueezy://audit-log`), and the predicate-destructive routing (e.g. `ls_update_license_key` with `activationLimit`). Previously every handler test invoked `tool.handler(input)` directly and the wrapper was untested.
96
121
 
97
- ## [0.9.0] 2026-05-14
122
+ ## [0.9.0] -- 2026-05-14
98
123
 
99
124
  ### Added
100
125
 
101
126
  - **Authority classes.** Every tool now declares an `authorityClass` describing the kind of business authority a caller needs to invoke it (`read`, `pii`, `mutate`, `money`, `recurring`, `key`, `webhook`). The class is separate from the existing destructive/read-only annotation.
102
- - `LEMONSQUEEZY_DISABLE_CLASSES` comma-separated list of classes to refuse outright. Example: `money,recurring,pii` lets an agent run reads but blocks refunds, subscription changes, and customer-record access. Unknown class names throw at server startup.
103
- - `LEMONSQUEEZY_RATE_LIMIT_PER_CLASS` per-class rolling rate limits with a DSL. Each entry is `class:N`, `class:N/m`, or `class:N/h`. Example: `money:2/h,recurring:5/h,key:10/m`. Composes with `LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT` both must pass. No LemonSqueezy permission can express "max 2 refunds per hour"; this is the only place that policy can live.
104
- - `deprecate.yml` workflow CI-driven `npm deprecate` via `workflow_dispatch`, using the org-level `NPM_TOKEN` and the same `release-npm` concurrency group as `release.yml`. No local WebAuthn session needed for a deprecate run.
105
- - `server.json` + `mcpName` field in `package.json` prepares the repo for the Official MCP Registry. Downstream registries (Glama, PulseMCP, mcpservers.org) auto-source from the official registry, so a single publish reaches the ecosystem.
106
- - README "Add to mcp.hosting" install button one-click sync of the server into [mcp.hosting](https://mcp.hosting) so it propagates to every MCP client a user has configured.
127
+ - `LEMONSQUEEZY_DISABLE_CLASSES` -- comma-separated list of classes to refuse outright. Example: `money,recurring,pii` lets an agent run reads but blocks refunds, subscription changes, and customer-record access. Unknown class names throw at server startup.
128
+ - `LEMONSQUEEZY_RATE_LIMIT_PER_CLASS` -- per-class rolling rate limits with a DSL. Each entry is `class:N`, `class:N/m`, or `class:N/h`. Example: `money:2/h,recurring:5/h,key:10/m`. Composes with `LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT` -- both must pass. No LemonSqueezy permission can express "max 2 refunds per hour"; this is the only place that policy can live.
129
+ - `deprecate.yml` workflow -- CI-driven `npm deprecate` via `workflow_dispatch`, using the org-level `NPM_TOKEN` and the same `release-npm` concurrency group as `release.yml`. No local WebAuthn session needed for a deprecate run.
130
+ - `server.json` + `mcpName` field in `package.json` -- prepares the repo for the Official MCP Registry. Downstream registries (Glama, PulseMCP, mcpservers.org) auto-source from the official registry, so a single publish reaches the ecosystem.
131
+ - README "Add to mcp.hosting" install button -- one-click sync of the server into [mcp.hosting](https://mcp.hosting) so it propagates to every MCP client a user has configured.
107
132
 
108
133
  ### Changed
109
134
 
110
135
  - README "Authority classes" section reframed to lead with the authoritative control (a scoped LemonSqueezy API key) and present the env vars as defense in depth. `RATE_LIMIT_PER_CLASS` is called out as load-bearing (no LS access-control equivalent); `DISABLE_CLASSES` is positioned as a fast deploy-time alternative when the cost of a key rotation outweighs the strength gained.
111
136
  - `release.yml` smoke test now retries the actual `npx --version` call (30 × 10s) instead of gating on `npm view`. The two paths hit different CDN caches; `npm view` can clear while `npx` still ETARGETs on a stale mirror. Mirrors the pattern in `aws-mcp` / `tailscale-mcp`.
112
137
 
113
- ## [0.8.1] 2026-05-13
138
+ ## [0.8.1] -- 2026-05-13
114
139
 
115
140
  ### Fixed
116
141
 
117
142
  - `handlers.test.ts` isolation: CI's integration job sets `LEMONSQUEEZY_TEST_API_KEY` from a repo secret. With the priority chain introduced in 0.8.0 (`COMMAND` > `TEST_API_KEY` > `API_KEY`), that injected value took precedence over the in-test stub and broke three handler tests (`ls_get_user` bearer-token assertion plus the missing/empty-key cases). The suite-level `before()` now saves and clears all three source env vars, restores them in `after()`, and resets the secret cache. Reproducible locally by running with `LEMONSQUEEZY_TEST_API_KEY=ci-stub-key LEMONSQUEEZY_API_KEY="" npm test`.
118
143
 
119
- ## [0.8.0] 2026-05-13
144
+ ## [0.8.0] -- 2026-05-13
120
145
 
121
- Distribution-readiness pass closes feature gaps against other OSS LemonSqueezy MCP servers ahead of broader distribution.
146
+ Distribution-readiness pass -- closes feature gaps against other OSS LemonSqueezy MCP servers ahead of broader distribution.
122
147
 
123
148
  ### Added
124
149
 
125
150
  - **Smithery one-click install.** `smithery.yaml` declares the stdio start command + JSON Schema for env vars; Smithery generates the config UX. Install via `npx -y @smithery/cli install @yawlabs/lemonsqueezy-mcp --client claude`.
126
151
  - **Container images.** `Dockerfile` + `Containerfile` + `.dockerignore`. Multi-stage build on `node:20-alpine`, runs as the non-root `node` user, copies only `dist/index.js` into the runtime image. Stdio transport; no port exposed. Pass env via `-e LEMONSQUEEZY_*`.
127
- - `LEMONSQUEEZY_TEST_API_KEY` separate test-mode key. Priority sits between `LEMONSQUEEZY_API_KEY_COMMAND` (highest) and `LEMONSQUEEZY_API_KEY` (lowest), so a developer can point the server at a sandbox store without unsetting their production key. A one-shot stderr `test_mode` notice fires on first activation per process.
128
- - **MCP Resource `lemonsqueezy://audit-log`** bounded ring buffer (last 1000 entries, most-recent-first) of destructive-call audit entries, exposed as `application/x-ndjson`. Lets clients without stderr access retrieve the audit trail structurally. Secret-shaped input fields are already redacted before they reach the buffer.
152
+ - `LEMONSQUEEZY_TEST_API_KEY` -- separate test-mode key. Priority sits between `LEMONSQUEEZY_API_KEY_COMMAND` (highest) and `LEMONSQUEEZY_API_KEY` (lowest), so a developer can point the server at a sandbox store without unsetting their production key. A one-shot stderr `test_mode` notice fires on first activation per process.
153
+ - **MCP Resource `lemonsqueezy://audit-log`** -- bounded ring buffer (last 1000 entries, most-recent-first) of destructive-call audit entries, exposed as `application/x-ndjson`. Lets clients without stderr access retrieve the audit trail structurally. Secret-shaped input fields are already redacted before they reach the buffer.
129
154
 
130
155
  ### Changed
131
156
 
132
157
  - Dev-dependency audit warnings cleared via `npm audit fix`. Runtime bundle is unaffected (zero runtime deps).
133
158
 
134
- ## [0.7.1] 2026-05-13
159
+ ## [0.7.1] -- 2026-05-13
135
160
 
136
161
  Hardening pass on the v0.4.0 guardrails after observing realistic agent failure modes.
137
162
 
138
163
  ### Added
139
164
 
140
165
  - **Retry deadline.** `OVERALL_DEADLINE_MS=90s` ceiling on total wall clock across retries. The timeout error reports actual elapsed and attempt count (`Request timed out after Xs (N attempts)`) instead of the per-attempt budget. The loop stops on 5xx/429/timeout once the deadline is reached rather than starting a new attempt or sleeping past it.
141
- - **Audit redaction (`src/redact.ts`).** Masks values of secret-shaped keys (`secret`, `password`, `token`, `api[_-]?key`, `bearer`, `authorization`, `signing[_-]?secret` case-insensitive, whole-word) before the entry reaches the audit log. Anchored regex so business identifiers (`licenseKey`, `instanceId`, `storeId`) are preserved. Cycle-safe via `WeakSet` with a 32-depth cap. Defense in depth no destructive tool today carries a secret-typed input, but flipping any webhook tool to destructive would otherwise leak its signing secret.
166
+ - **Audit redaction (`src/redact.ts`).** Masks values of secret-shaped keys (`secret`, `password`, `token`, `api[_-]?key`, `bearer`, `authorization`, `signing[_-]?secret` -- case-insensitive, whole-word) before the entry reaches the audit log. Anchored regex so business identifiers (`licenseKey`, `instanceId`, `storeId`) are preserved. Cycle-safe via `WeakSet` with a 32-depth cap. Defense in depth -- no destructive tool today carries a secret-typed input, but flipping any webhook tool to destructive would otherwise leak its signing secret.
142
167
  - **`requiredFilters` scoping.** When `LEMONSQUEEZY_ALLOWED_STORE_IDS` is active, list tools that lack a `storeId` field must specify at least one parent filter (orderId, productId, variantId, etc.). Closes the silent-no-op gap on `ls_list_files` / `ls_list_prices` / `ls_list_variants` / `ls_list_order_items` / `ls_list_subscription_items` / `ls_list_usage_records` / `ls_list_discount_redemptions` / `ls_list_license_key_instances`. Each affected tool description flags the cross-store consideration. `ls_list_affiliates` has no meaningful parent filter and is description-only.
143
168
 
144
169
  ### Changed
@@ -146,11 +171,11 @@ Hardening pass on the v0.4.0 guardrails after observing realistic agent failure
146
171
  - `ls_update_license_key` is now classified as destructive whenever `activationLimit` changes, not just when `disabled: true`. Shrinking the limit can revoke access and the input alone doesn't reveal shrink-vs-grow direction.
147
172
  - `ls_create_checkout` email validation tightened to `.email().max(320)` to match `ls_create_customer` (was `max(10000)` with no shape check).
148
173
 
149
- ## [0.7.0] 2026-05-06
174
+ ## [0.7.0] -- 2026-05-06
150
175
 
151
176
  ### Added
152
177
 
153
- - `LEMONSQUEEZY_LOG=audit|error|all` fine-grained log levels for long-running deployments. `audit` keeps destructive-call entries plus errors and drops successful reads (recommended for production where log volume matters over weeks). `error` keeps only failures. `all` is the verbose default. `json` is retained as a backwards-compat alias for `all`.
178
+ - `LEMONSQUEEZY_LOG=audit|error|all` -- fine-grained log levels for long-running deployments. `audit` keeps destructive-call entries plus errors and drops successful reads (recommended for production where log volume matters over weeks). `error` keeps only failures. `all` is the verbose default. `json` is retained as a backwards-compat alias for `all`.
154
179
 
155
180
  ### Changed
156
181
 
@@ -161,24 +186,24 @@ Hardening pass on the v0.4.0 guardrails after observing realistic agent failure
161
186
 
162
187
  ### Fixed
163
188
 
164
- - `release.sh` annotated tags (`git tag -a`) `git push --follow-tags` silently skips lightweight tags. Pre-0.7.0, the release commit could push without the tag, leaving CI release unfired. Caught by review before any failed run.
165
- - `release.sh` idempotency check now queries `@yawlabs/<pkg>@${VERSION}` specifically rather than the package's `latest` dist-tag. The bare query returned whichever version is latest on the registry, so an out-of-band higher version made the script try to re-publish the current one and fail with "cannot publish over previously published version." The versioned form returns the version when it exists and empty otherwise correct idempotency semantics.
189
+ - `release.sh` annotated tags (`git tag -a`) -- `git push --follow-tags` silently skips lightweight tags. Pre-0.7.0, the release commit could push without the tag, leaving CI release unfired. Caught by review before any failed run.
190
+ - `release.sh` idempotency check now queries `@yawlabs/<pkg>@${VERSION}` specifically rather than the package's `latest` dist-tag. The bare query returned whichever version is latest on the registry, so an out-of-band higher version made the script try to re-publish the current one and fail with "cannot publish over previously published version." The versioned form returns the version when it exists and empty otherwise -- correct idempotency semantics.
166
191
  - `release.sh` push now uses `--follow-tags` instead of `--tags` so stale local tags don't ride along (`--tags` pushes every local tag).
167
192
  - `release.yml` concurrency group is now a literal `release-npm` (workflow-scoped); previously `release-${{ github.ref }}` evaluated to per-tag groups and back-to-back tag pushes would race on `npm publish`.
168
- - `ci.yml` no longer runs a redundant explicit `npm run build` step `npm test` is `npm run build && node --test dist/...` for this dist-based repo, so the explicit step was building twice per matrix cell for zero signal.
193
+ - `ci.yml` no longer runs a redundant explicit `npm run build` step -- `npm test` is `npm run build && node --test dist/...` for this dist-based repo, so the explicit step was building twice per matrix cell for zero signal.
169
194
 
170
195
  ### Restored
171
196
 
172
197
  - `CODEOWNERS` and `dependabot.yml` (dropped along with the workflows in 0.6.0; restored when the workflows came back in 0.6.1). Routes review requests to `@jeffyaw` and bumps npm deps weekly / github-actions deps monthly.
173
198
 
174
- ## [0.6.2] 2026-05-04
199
+ ## [0.6.2] -- 2026-05-04
175
200
 
176
201
  ### Changed
177
202
 
178
203
  - `release.yml` now runs a post-publish smoke test that fetches the just-published tarball via `npx -y @yawlabs/lemonsqueezy-mcp@<version> --version` and asserts the binary executes and prints the expected version. Catches packaging regressions (missing bin shebang, broken `files` entry, bad esbuild output) before they reach real users.
179
204
  - `release.sh` step 7 now verifies that CI publishes carry a sigstore provenance attestation. A missing attestation in CI mode is a soft warning; local publishes legitimately skip provenance.
180
205
 
181
- ## [0.6.1] 2026-05-04
206
+ ## [0.6.1] -- 2026-05-04
182
207
 
183
208
  ### Changed
184
209
 
@@ -192,7 +217,7 @@ Hardening pass on the v0.4.0 guardrails after observing realistic agent failure
192
217
  - `release.sh` `npm publish` retry only fires on EOTP/EAUTH/OTP messages. Other failures (duplicate-version E403, packaging errors) bail immediately instead of wasting 60s in the retry loop.
193
218
  - `package.json` `prepublishOnly` trimmed to `npm run build`. `release.sh` already runs lint + tests before publishing in both modes, so the embedded test run was doubling the CI work per release.
194
219
 
195
- ## [0.6.0] 2026-04-24
220
+ ## [0.6.0] -- 2026-04-24
196
221
 
197
222
  ### Changed
198
223
 
@@ -210,18 +235,18 @@ Hardening pass on the v0.4.0 guardrails after observing realistic agent failure
210
235
 
211
236
  ### Removed
212
237
 
213
- - All of `.github/` (workflows, dependabot, CODEOWNERS). There is no CI `release.sh` is the only supported release path.
238
+ - All of `.github/` (workflows, dependabot, CODEOWNERS). There is no CI -- `release.sh` is the only supported release path.
214
239
  - `test:ci` npm script.
215
240
 
216
241
  ### Docs
217
242
 
218
243
  - README documents the local release flow and the one-time `npm login` / `gh auth login` setup.
219
- - README clarifies `LEMONSQUEEZY_ALLOWED_STORE_IDS` semantics list filters now required when the allowlist is set; tools with no `storeId` field at all remain ungated, so pair with the refund cap and rate limit.
244
+ - README clarifies `LEMONSQUEEZY_ALLOWED_STORE_IDS` semantics -- list filters now required when the allowlist is set; tools with no `storeId` field at all remain ungated, so pair with the refund cap and rate limit.
220
245
  - README notes that `LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT` counts include `ls_update_license_key` with `disabled: true`.
221
246
  - `SEMVER.md` points at `npm run test:integration` for upstream-drift detection instead of the removed nightly workflow.
222
247
  - `CLAUDE.md` and `CONTRIBUTING.md` updated to reference the local script and Biome-on-review instead of CI checks.
223
248
 
224
- ## [0.5.0] 2026-04-23
249
+ ## [0.5.0] -- 2026-04-23
225
250
 
226
251
  ### Changed
227
252
 
@@ -238,7 +263,7 @@ Hardening pass on the v0.4.0 guardrails after observing realistic agent failure
238
263
 
239
264
  - README tool count corrected from 59 to 61 (affiliate tools from 0.3.0 plus `ls_refund_subscription_invoice` weren't reflected in the total).
240
265
 
241
- ## [0.4.1] 2026-04-20
266
+ ## [0.4.1] -- 2026-04-20
242
267
 
243
268
  ### Security
244
269
 
@@ -248,23 +273,23 @@ Hardening pass on the v0.4.0 guardrails after observing realistic agent failure
248
273
 
249
274
  - README links to `@yawlabs/lemonsqueezy-webhook-sink` from the webhook-reconciliation callout.
250
275
 
251
- ## [0.4.0] 2026-04-20
276
+ ## [0.4.0] -- 2026-04-20
252
277
 
253
278
  Hardening pass for unattended automation against live billing flows.
254
279
 
255
280
  ### Added
256
281
 
257
282
  - **Guardrails.** Opt-in controls evaluated in a single dispatcher pre-check:
258
- - `LEMONSQUEEZY_ALLOWED_STORE_IDS` allowlist enforced on every tool call that names a store.
259
- - `LEMONSQUEEZY_MAX_REFUND_AMOUNT_CENTS` per-call cap on `ls_refund_order` to prevent runaway agents from issuing large refunds.
260
- - `LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT` rolling 60-second circuit breaker on destructive tool calls.
283
+ - `LEMONSQUEEZY_ALLOWED_STORE_IDS` -- allowlist enforced on every tool call that names a store.
284
+ - `LEMONSQUEEZY_MAX_REFUND_AMOUNT_CENTS` -- per-call cap on `ls_refund_order` to prevent runaway agents from issuing large refunds.
285
+ - `LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT` -- rolling 60-second circuit breaker on destructive tool calls.
261
286
  All three default to unset → disabled, so existing integrations are unaffected until explicitly opted in.
262
287
  - **Retry layer** (`src/retry.ts`). Exponential backoff with jitter, capped at 4 attempts and 30s. Retries `429` always (honors `Retry-After`), `5xx` and transport errors only on idempotent methods (`GET`/`DELETE`). Non-idempotent writes fail fast.
263
288
  - **Secret loader** (`src/secret.ts`). `LEMONSQUEEZY_API_KEY_COMMAND` invokes an external command (vault CLI, 1Password, etc.) and caches the result for 1 hour. Lets credentials rotate without restarting the server.
264
289
  - **Structured logger** (`src/logger.ts`). Opt-in via `LEMONSQUEEZY_LOG=json`. Emits JSON lines to stderr (stdout stays reserved for MCP protocol). Destructive tool calls are tagged `audit: true` with redacted inputs.
265
290
  - **Request ID surfacing.** API error messages include upstream `X-Request-Id` when present, so support tickets can be traced.
266
291
  - **Read-only integration tests.** `npm run test:integration` hits a live LemonSqueezy store if `LEMONSQUEEZY_TEST_API_KEY` + `LEMONSQUEEZY_TEST_STORE_ID` are set; skips gracefully otherwise. Exercises `ls_get_user`, `ls_get_store`, `ls_list_products`, `ls_list_variants`, `ls_list_orders`, `ls_list_subscriptions`, and 404 error paths. Runs nightly via `.github/workflows/integration.yml`.
267
- - **`SEMVER.md`** documents what counts as a breaking change for this package (tool names, required inputs, return shapes), and what explicitly does not (upstream API drift, internal module names).
292
+ - **`SEMVER.md`** -- documents what counts as a breaking change for this package (tool names, required inputs, return shapes), and what explicitly does not (upstream API drift, internal module names).
268
293
 
269
294
  ### Changed
270
295
 
@@ -273,18 +298,18 @@ Hardening pass for unattended automation against live billing flows.
273
298
  ### Fixed
274
299
 
275
300
  - `parseRetryAfterMs` now correctly falls back to the default 1s when given a negative number like `"-3"` (previously `Date.parse("-3")` returned a finite value and produced a non-sensical delay).
276
- - Retry policy no longer retries `5xx` on `POST`/`PATCH`/`PUT` prevents duplicate writes if a timeout is actually a slow success.
301
+ - Retry policy no longer retries `5xx` on `POST`/`PATCH`/`PUT` -- prevents duplicate writes if a timeout is actually a slow success.
277
302
 
278
- ## [0.3.0] 2026-04-18
303
+ ## [0.3.0] -- 2026-04-18
279
304
 
280
305
  ### Added
281
306
 
282
307
  - Affiliate tools (`ls_list_affiliates`, `ls_get_affiliate`).
283
308
  - `429` retry with exponential backoff in the API client.
284
- - `SECURITY.md` vulnerability disclosure policy.
285
- - `CONTRIBUTING.md` contributor and AI-agent guidelines.
309
+ - `SECURITY.md` -- vulnerability disclosure policy.
310
+ - `CONTRIBUTING.md` -- contributor and AI-agent guidelines.
286
311
 
287
- ## [0.2.1] 2026-04-16
312
+ ## [0.2.1] -- 2026-04-16
288
313
 
289
314
  ### Changed
290
315
 
@@ -298,7 +323,7 @@ Hardening pass for unattended automation against live billing flows.
298
323
 
299
324
  - Error-path tests for every tool.
300
325
 
301
- ## [0.2.0] 2026-04-14
326
+ ## [0.2.0] -- 2026-04-14
302
327
 
303
328
  ### Added
304
329
 
@@ -308,17 +333,19 @@ Hardening pass for unattended automation against live billing flows.
308
333
 
309
334
  - `ls_generate_order_invoice` and `ls_generate_subscription_invoice` now hit the correct endpoints and handle the async invoice-generation response shape.
310
335
 
311
- ## [0.1.1] 2026-04-12
336
+ ## [0.1.1] -- 2026-04-12
312
337
 
313
338
  ### Added
314
339
 
315
340
  - Edge-case handler tests for fuller coverage across all 59 tools.
316
341
 
317
- ## [0.1.0] 2026-04-11
342
+ ## [0.1.0] -- 2026-04-11
318
343
 
319
344
  Initial release. 59 tools covering all 17 LemonSqueezy API resources.
320
345
 
321
- [Unreleased]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.5...HEAD
346
+ [Unreleased]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.7...HEAD
347
+ [0.10.7]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.6...v0.10.7
348
+ [0.10.6]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.5...v0.10.6
322
349
  [0.10.5]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.4...v0.10.5
323
350
  [0.10.4]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.3...v0.10.4
324
351
  [0.10.3]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.2...v0.10.3
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;
@@ -30684,10 +30697,10 @@ function buildQuery(params) {
30684
30697
  }
30685
30698
  if (params.page) {
30686
30699
  if (params.page.number !== void 0) {
30687
- parts.push(`page[number]=${params.page.number}`);
30700
+ parts.push(`page[number]=${encodeURIComponent(String(params.page.number))}`);
30688
30701
  }
30689
30702
  if (params.page.size !== void 0) {
30690
- parts.push(`page[size]=${params.page.size}`);
30703
+ parts.push(`page[size]=${encodeURIComponent(String(params.page.size))}`);
30691
30704
  }
30692
30705
  }
30693
30706
  return parts.length > 0 ? `?${parts.join("&")}` : "";
@@ -30886,7 +30899,7 @@ function getHandler(endpoint, idField) {
30886
30899
  };
30887
30900
  }
30888
30901
  function listHandler(endpoint, filterMap = {}) {
30889
- return async (input) => {
30902
+ const handler = async (input) => {
30890
30903
  const filter = {};
30891
30904
  for (const [inputKey, apiKey] of Object.entries(filterMap)) {
30892
30905
  const val = input[inputKey];
@@ -30899,6 +30912,7 @@ function listHandler(endpoint, filterMap = {}) {
30899
30912
  });
30900
30913
  return apiGet(`${endpoint}${query}`);
30901
30914
  };
30915
+ return Object.assign(handler, { filterMap });
30902
30916
  }
30903
30917
  async function apiGet(path) {
30904
30918
  return apiRequest("GET", path);
@@ -31831,6 +31845,7 @@ var productTools = [
31831
31845
  // src/tools/sink.ts
31832
31846
  var SINK_REPO_URL = "https://github.com/YawLabs/lemonsqueezy-webhook-sink";
31833
31847
  var FETCH_TIMEOUT_MS = 1e4;
31848
+ var MAX_BODY_SIZE_BYTES = 10 * 1024 * 1024;
31834
31849
  function loadSinkConfig() {
31835
31850
  const rawUrl = process.env.LEMONSQUEEZY_SINK_URL;
31836
31851
  const token = process.env.LEMONSQUEEZY_SINK_ADMIN_TOKEN;
@@ -31910,6 +31925,12 @@ async function sinkRequest(config2, method, pathAndQuery) {
31910
31925
  }
31911
31926
  const text = await res.text();
31912
31927
  if (!text.trim()) return { ok: true, data: {} };
31928
+ if (text.length > MAX_BODY_SIZE_BYTES) {
31929
+ return {
31930
+ ok: false,
31931
+ error: `Sink response body too large: ${text.length} bytes exceeds ${MAX_BODY_SIZE_BYTES} byte limit`
31932
+ };
31933
+ }
31913
31934
  try {
31914
31935
  return { ok: true, data: JSON.parse(text) };
31915
31936
  } catch (err) {
@@ -32712,19 +32733,22 @@ function createToolHandler(tool) {
32712
32733
  checkStoreScopedToolInput(toolAsRecord, inputAsRecord);
32713
32734
  const response = await tool.handler(input);
32714
32735
  const latency_ms = Date.now() - start;
32715
- const successEntry = {
32716
- event: "tool_call",
32717
- tool: tool.name,
32718
- status: response.ok ? "ok" : "error",
32719
- latency_ms,
32720
- request_id: response.requestId,
32721
- error: response.ok ? void 0 : response.error,
32722
- audit: isDestructive ? true : void 0,
32723
- inputs: isDestructive ? redactSecrets(input) : void 0
32724
- };
32725
- logEvent(successEntry);
32726
- if (isDestructive) {
32727
- pushAuditEntry({ ts: (/* @__PURE__ */ new Date()).toISOString(), ...successEntry });
32736
+ const wouldLog = wouldLogToolCall({ isDestructive, isError: !response.ok });
32737
+ if (wouldLog || isDestructive) {
32738
+ const successEntry = {
32739
+ event: "tool_call",
32740
+ tool: tool.name,
32741
+ status: response.ok ? "ok" : "error",
32742
+ latency_ms,
32743
+ request_id: response.requestId,
32744
+ error: response.ok ? void 0 : response.error,
32745
+ audit: isDestructive ? true : void 0,
32746
+ inputs: isDestructive ? redactSecrets(input) : void 0
32747
+ };
32748
+ if (wouldLog) logEvent(successEntry);
32749
+ if (isDestructive) {
32750
+ pushAuditEntry({ ts: (/* @__PURE__ */ new Date()).toISOString(), ...successEntry });
32751
+ }
32728
32752
  }
32729
32753
  if (!response.ok) {
32730
32754
  return {
@@ -32744,18 +32768,21 @@ function createToolHandler(tool) {
32744
32768
  } catch (err) {
32745
32769
  const message = err instanceof Error ? err.message : String(err);
32746
32770
  const latency_ms = Date.now() - start;
32747
- const errorEntry = {
32748
- event: "tool_call",
32749
- tool: tool.name,
32750
- status: err instanceof GuardrailError ? "guardrail_block" : "exception",
32751
- latency_ms,
32752
- error: message,
32753
- audit: isDestructive ? true : void 0,
32754
- inputs: isDestructive ? redactSecrets(input) : void 0
32755
- };
32756
- logEvent(errorEntry);
32757
- if (isDestructive) {
32758
- pushAuditEntry({ ts: (/* @__PURE__ */ new Date()).toISOString(), ...errorEntry });
32771
+ const wouldLog = wouldLogToolCall({ isDestructive, isError: true });
32772
+ if (wouldLog || isDestructive) {
32773
+ const errorEntry = {
32774
+ event: "tool_call",
32775
+ tool: tool.name,
32776
+ status: err instanceof GuardrailError ? "guardrail_block" : "exception",
32777
+ latency_ms,
32778
+ error: message,
32779
+ audit: isDestructive ? true : void 0,
32780
+ inputs: isDestructive ? redactSecrets(input) : void 0
32781
+ };
32782
+ if (wouldLog) logEvent(errorEntry);
32783
+ if (isDestructive) {
32784
+ pushAuditEntry({ ts: (/* @__PURE__ */ new Date()).toISOString(), ...errorEntry });
32785
+ }
32759
32786
  }
32760
32787
  return {
32761
32788
  content: [{ type: "text", text: `Error: ${message}` }],
@@ -32779,7 +32806,7 @@ function readAuditLogResource(uri) {
32779
32806
  }
32780
32807
 
32781
32808
  // src/index.ts
32782
- var version2 = true ? "0.10.5" : (await null).createRequire(import.meta.url)("../package.json").version;
32809
+ var version2 = true ? "0.10.7" : (await null).createRequire(import.meta.url)("../package.json").version;
32783
32810
  var subcommand = process.argv[2];
32784
32811
  if (subcommand === "version" || subcommand === "--version") {
32785
32812
  console.log(version2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yawlabs/lemonsqueezy-mcp",
3
- "version": "0.10.5",
3
+ "version": "0.10.7",
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",