@yawlabs/lemonsqueezy-mcp 0.10.0 → 0.10.2

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 +316 -0
  2. package/dist/index.js +185 -181
  3. package/package.json +3 -2
package/CHANGELOG.md ADDED
@@ -0,0 +1,316 @@
1
+ # Changelog
2
+
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
+
5
+ ## [0.10.2] -- 2026-05-16
6
+
7
+ ### Changed
8
+
9
+ - **`CHANGELOG.md` is now included in the published npm tarball.** Added it to `package.json`'s `files` array. npm auto-includes `README` and `LICENSE` regardless of the allowlist, but `CHANGELOG.md` is not in that auto-include set -- so prior versions shipped without it, and an operator inspecting `node_modules/@yawlabs/lemonsqueezy-mcp` had no in-package changelog. The README and bundle are unchanged; tarball size grows by ~29 kB.
10
+
11
+ ## [0.10.1] -- 2026-05-16
12
+
13
+ ### Fixed
14
+
15
+ - **`smithery.yaml` now exposes the sink-bridge env vars.** Added `lemonsqueezySinkUrl` and `lemonsqueezySinkAdminToken` to `configSchema.properties` and forwarded them in `commandFunction`. Without this, Smithery-installed instances could not configure the 0.10.0 `ls_sink_*` tools and they always returned "not configured".
16
+ - **Guardrail env vars are now parsed at server boot, not first call.** A new `loadGuardrailOptions()` export is invoked from `src/index.ts` before stdio connect, so a typo'd `LEMONSQUEEZY_DISABLE_CLASSES`, malformed `LEMONSQUEEZY_RATE_LIMIT_PER_CLASS`, or non-numeric `LEMONSQUEEZY_MAX_REFUND_AMOUNT_CENTS` crashes the process at startup. Previously the parse was lazy and a misconfigured deployment booted cleanly, passing liveness probes, and only surfaced the error on the first tool invocation.
17
+
18
+ ## [0.10.0] -- 2026-05-15
19
+
20
+ ### Added
21
+
22
+ - **Webhook-sink bridge tools** for the optional [@yawlabs/lemonsqueezy-webhook-sink](https://github.com/YawLabs/lemonsqueezy-webhook-sink) process. Three new tools give an agent a unified read surface for "what webhooks have actually fired" alongside the existing management-API reads:
23
+ - `ls_sink_events_list` -- list received webhook events with optional `since` / `type` / `limit` filters
24
+ - `ls_sink_event_mark_processed` -- idempotent ack from your consumer
25
+ - `ls_sink_stats` -- total, unprocessed, last-received timestamp
26
+ - `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.
27
+
28
+ ## [0.9.3] — 2026-05-15
29
+
30
+ ### Fixed
31
+
32
+ - `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`.
33
+
34
+ ## [0.9.2] — 2026-05-15
35
+
36
+ ### Added
37
+
38
+ - `.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.
39
+
40
+ ### Docs
41
+
42
+ - README Resources section now spells out the full redaction policy (every secret-named key the regex matches, plus JWT-shaped string values), so an operator who sees a `[REDACTED]` under an innocuous key knows where it came from.
43
+ - README Development section documents `npm run gen:containerfile` / `npm run check:containerfile` so a contributor editing `Dockerfile` knows to regenerate.
44
+ - 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.
45
+
46
+ ## [0.9.1] — 2026-05-15
47
+
48
+ ### Docs
49
+
50
+ - 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.
51
+ - 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.
52
+ - 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).
53
+
54
+ ### Changed
55
+
56
+ - `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.
57
+ - `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.
58
+ - `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.
59
+ - `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`).
60
+
61
+ ### Fixed
62
+
63
+ - 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.
64
+ - 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."
65
+ - `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.
66
+ - `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`.
67
+ - `Containerfile` is now generated from `Dockerfile` via `scripts/sync-containerfile.mjs`. `npm run check:containerfile` (called by `release.sh` and CI) verifies the two stay in sync; `npm run gen:containerfile` regenerates. Removes the byte-identical-but-not-actually drift surface where editing one file silently left the other behind.
68
+
69
+ ### Added
70
+
71
+ - `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.
72
+ - `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.
73
+
74
+ ## [0.9.0] — 2026-05-14
75
+
76
+ ### Added
77
+
78
+ - **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.
79
+ - `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.
80
+ - `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.
81
+ - `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.
82
+ - `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.
83
+ - 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.
84
+
85
+ ### Changed
86
+
87
+ - 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.
88
+ - `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`.
89
+
90
+ ## [0.8.1] — 2026-05-13
91
+
92
+ ### Fixed
93
+
94
+ - `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`.
95
+
96
+ ## [0.8.0] — 2026-05-13
97
+
98
+ Distribution-readiness pass — closes feature gaps against other OSS LemonSqueezy MCP servers ahead of broader distribution.
99
+
100
+ ### Added
101
+
102
+ - **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`.
103
+ - **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_*`.
104
+ - `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.
105
+ - **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.
106
+
107
+ ### Changed
108
+
109
+ - Dev-dependency audit warnings cleared via `npm audit fix`. Runtime bundle is unaffected (zero runtime deps).
110
+
111
+ ## [0.7.1] — 2026-05-13
112
+
113
+ Hardening pass on the v0.4.0 guardrails after observing realistic agent failure modes.
114
+
115
+ ### Added
116
+
117
+ - **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.
118
+ - **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.
119
+ - **`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.
120
+
121
+ ### Changed
122
+
123
+ - `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.
124
+ - `ls_create_checkout` email validation tightened to `.email().max(320)` to match `ls_create_customer` (was `max(10000)` with no shape check).
125
+
126
+ ## [0.7.0] — 2026-05-06
127
+
128
+ ### Added
129
+
130
+ - `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`.
131
+
132
+ ### Changed
133
+
134
+ - Tool ID validation now uses a shared `lsIdSchema` (numeric string regex) so a typo fails at the schema layer with a clear message instead of as an opaque 422 from the upstream API.
135
+ - `ls_update_customer.status` tightened to `z.literal("archived")` so the destructive predicate cannot be side-channeled by an unrecognized status string.
136
+ - Secret cache refactored to a fingerprint-keyed entry. `invalidateApiKeyCache()` (called from `api.ts` on 401/403 responses) now behaves uniformly across the env, test, and command source modes. A rotated upstream key picks up on the next request without waiting on the 1h TTL.
137
+ - Integration-test unique suffix switched from a timestamp slice to `crypto.randomUUID().slice(0, 8)`. Two overlapping CI runs (nightly + `workflow_dispatch`) that started in the same millisecond no longer collide on resource naming.
138
+
139
+ ### Fixed
140
+
141
+ - `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.
142
+ - `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.
143
+ - `release.sh` push now uses `--follow-tags` instead of `--tags` so stale local tags don't ride along (`--tags` pushes every local tag).
144
+ - `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`.
145
+ - `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.
146
+
147
+ ### Restored
148
+
149
+ - `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.
150
+
151
+ ## [0.6.2] — 2026-05-04
152
+
153
+ ### Changed
154
+
155
+ - `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.
156
+ - `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.
157
+
158
+ ## [0.6.1] — 2026-05-04
159
+
160
+ ### Changed
161
+
162
+ - Restored CI release plumbing (`ci.yml`, `integration.yml`, `release.yml`) ported from `tailscale-mcp`. Tag-and-let-CI is the preferred release path; local `release.sh <version>` still works for end-to-end runs from the workstation.
163
+ - `release.sh` gained a CI mode that derives version from `$GITHUB_REF_NAME`, skips local-only gates, and publishes with `--provenance`.
164
+
165
+ ### Fixed
166
+
167
+ - `integration.yml` now requires both `LEMONSQUEEZY_TEST_API_KEY` and `LEMONSQUEEZY_TEST_STORE_ID` together. A half-configured repo previously ran the workflow successfully with every integration suite silently skipped, masking zero coverage as green CI.
168
+ - `release.sh` CI mode now hard-fails when `package.json` disagrees with the pushed tag instead of bumping inside the ephemeral checkout, which would have published the right version while leaving `main` pointing at the old one.
169
+ - `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.
170
+ - `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.
171
+
172
+ ## [0.6.0] — 2026-04-24
173
+
174
+ ### Changed
175
+
176
+ - **Breaking.** `LEMONSQUEEZY_ALLOWED_STORE_IDS` now requires `storeId` on list tools that accept it as a filter (`ls_list_orders`, `ls_list_subscriptions`, etc.). Previously the allowlist was silently bypassed when callers omitted the optional filter, returning data from every store the API key could see. Callers that relied on the unfiltered behavior must now pass an allowed `storeId` explicitly.
177
+ - `ls_update_license_key` calls that set `disabled: true` are now classified as destructive at runtime, so revocations engage `LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT` and the audit log. Benign edits (expiry, activation limit) stay on the regular path.
178
+ - `ls_update_subscription` rejects non-numeric `variantId` at the schema level instead of producing `NaN` upstream.
179
+ - `release.sh` rewritten as a local-only, idempotent deploy with branch and auth pre-flight, EOTP retry on `npm publish`, and clearer failure messages. Re-running with the same version after a partial failure resumes from where it stopped.
180
+
181
+ ### Fixed
182
+
183
+ - Path segments are URL-encoded across `getHandler` and every inline tool handler via a new `encodePath()` helper, closing a path-injection surface where IDs containing `/` could target adjacent endpoints.
184
+ - Logger emits a degraded fallback entry on `JSON.stringify` failure so destructive-call audit trails survive circular inputs.
185
+ - Checkout `billing_address` composition is now order-independent.
186
+ - Integration test env mutation moved from module-eval to a `before()` hook.
187
+
188
+ ### Removed
189
+
190
+ - All of `.github/` (workflows, dependabot, CODEOWNERS). There is no CI — `release.sh` is the only supported release path.
191
+ - `test:ci` npm script.
192
+
193
+ ### Docs
194
+
195
+ - README documents the local release flow and the one-time `npm login` / `gh auth login` setup.
196
+ - 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.
197
+ - README notes that `LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT` counts include `ls_update_license_key` with `disabled: true`.
198
+ - `SEMVER.md` points at `npm run test:integration` for upstream-drift detection instead of the removed nightly workflow.
199
+ - `CLAUDE.md` and `CONTRIBUTING.md` updated to reference the local script and Biome-on-review instead of CI checks.
200
+
201
+ ## [0.5.0] — 2026-04-23
202
+
203
+ ### Changed
204
+
205
+ - **Breaking.** `ls_update_webhook` now validates `secret` as `min(6).max(40)`, matching `ls_create_webhook`. Previously accepted any string up to 10,000 chars. Callers passing secrets outside the 6-40 range will now be rejected at the MCP boundary.
206
+ - **Breaking.** Email fields across `ls_list_customers`, `ls_create_customer`, `ls_update_customer`, `ls_list_orders`, and `ls_list_subscriptions` now validate as RFC email (`z.string().email().max(320)`), matching the existing `ls_list_affiliates` filter. Non-email inputs (partial matches, malformed addresses) will now be rejected at the MCP boundary.
207
+ - `LEMONSQUEEZY_MAX_REFUND_AMOUNT_CENTS` guardrail now also applies to `ls_refund_subscription_invoice`, closing a gap where the cap only gated `ls_refund_order`.
208
+ - `ls_update_subscription` `trialEndsAt` is now `.nullable()` at the schema level. The description always promised `null` ends the trial immediately; the type now supports it.
209
+
210
+ ### Fixed
211
+
212
+ - Integration test env-var restoration now runs in a top-level `after()` hook instead of at module scope, so `LEMONSQUEEZY_API_KEY` stays set to the test key for the duration of the test run. Previously a developer with both `LEMONSQUEEZY_API_KEY` (prod) and `LEMONSQUEEZY_TEST_API_KEY` set could have their prod key used against the test store.
213
+
214
+ ### Docs
215
+
216
+ - 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).
217
+
218
+ ## [0.4.1] — 2026-04-20
219
+
220
+ ### Security
221
+
222
+ - Override transitive `hono` to `^4.12.14` to clear Dependabot advisory on `hono/jsx` SSR. Not exploitable in this package (MCP does not use `hono/jsx`), but closes the supply-chain scan signal.
223
+
224
+ ### Docs
225
+
226
+ - README links to `@yawlabs/lemonsqueezy-webhook-sink` from the webhook-reconciliation callout.
227
+
228
+ ## [0.4.0] — 2026-04-20
229
+
230
+ Hardening pass for unattended automation against live billing flows.
231
+
232
+ ### Added
233
+
234
+ - **Guardrails.** Opt-in controls evaluated in a single dispatcher pre-check:
235
+ - `LEMONSQUEEZY_ALLOWED_STORE_IDS` — allowlist enforced on every tool call that names a store.
236
+ - `LEMONSQUEEZY_MAX_REFUND_AMOUNT_CENTS` — per-call cap on `ls_refund_order` to prevent runaway agents from issuing large refunds.
237
+ - `LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT` — rolling 60-second circuit breaker on destructive tool calls.
238
+ All three default to unset → disabled, so existing integrations are unaffected until explicitly opted in.
239
+ - **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.
240
+ - **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.
241
+ - **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.
242
+ - **Request ID surfacing.** API error messages include upstream `X-Request-Id` when present, so support tickets can be traced.
243
+ - **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`.
244
+ - **`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).
245
+
246
+ ### Changed
247
+
248
+ - Input validation on every tool: `z.string()` fields capped at 10,000 chars, `.email()` fields capped at 320 chars. Protects against accidental DoS via oversized payloads.
249
+
250
+ ### Fixed
251
+
252
+ - `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).
253
+ - Retry policy no longer retries `5xx` on `POST`/`PATCH`/`PUT` — prevents duplicate writes if a timeout is actually a slow success.
254
+
255
+ ## [0.3.0] — 2026-04-18
256
+
257
+ ### Added
258
+
259
+ - Affiliate tools (`ls_list_affiliates`, `ls_get_affiliate`).
260
+ - `429` retry with exponential backoff in the API client.
261
+ - `SECURITY.md` — vulnerability disclosure policy.
262
+ - `CONTRIBUTING.md` — contributor and AI-agent guidelines.
263
+
264
+ ## [0.2.1] — 2026-04-16
265
+
266
+ ### Changed
267
+
268
+ - Deduplicated tool handler boilerplate across tool files.
269
+
270
+ ### Fixed
271
+
272
+ - API error handling surfaces upstream error bodies correctly.
273
+
274
+ ### Added
275
+
276
+ - Error-path tests for every tool.
277
+
278
+ ## [0.2.0] — 2026-04-14
279
+
280
+ ### Added
281
+
282
+ - Input validation via Zod `.describe()` on every tool input field.
283
+
284
+ ### Fixed
285
+
286
+ - `ls_generate_order_invoice` and `ls_generate_subscription_invoice` now hit the correct endpoints and handle the async invoice-generation response shape.
287
+
288
+ ## [0.1.1] — 2026-04-12
289
+
290
+ ### Added
291
+
292
+ - Edge-case handler tests for fuller coverage across all 59 tools.
293
+
294
+ ## [0.1.0] — 2026-04-11
295
+
296
+ Initial release. 59 tools covering all 17 LemonSqueezy API resources.
297
+
298
+ [Unreleased]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.9.3...HEAD
299
+ [0.9.3]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.9.2...v0.9.3
300
+ [0.9.2]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.9.1...v0.9.2
301
+ [0.9.1]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.9.0...v0.9.1
302
+ [0.9.0]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.8.1...v0.9.0
303
+ [0.8.1]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.7.1...v0.8.1
304
+ [0.7.1]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.7.0...v0.7.1
305
+ [0.7.0]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.6.2...v0.7.0
306
+ [0.6.2]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.6.1...v0.6.2
307
+ [0.6.1]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.6.0...v0.6.1
308
+ [0.6.0]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.5.0...v0.6.0
309
+ [0.5.0]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.4.1...v0.5.0
310
+ [0.4.1]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.4.0...v0.4.1
311
+ [0.4.0]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.3.0...v0.4.0
312
+ [0.3.0]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.2.1...v0.3.0
313
+ [0.2.1]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.2.0...v0.2.1
314
+ [0.2.0]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.1.1...v0.2.0
315
+ [0.1.1]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.1.0...v0.1.1
316
+ [0.1.0]: https://github.com/YawLabs/lemonsqueezy-mcp/releases/tag/v0.1.0
package/dist/index.js CHANGED
@@ -30200,6 +30200,189 @@ var StdioServerTransport = class {
30200
30200
  }
30201
30201
  };
30202
30202
 
30203
+ // src/guardrails.ts
30204
+ var GuardrailError = class extends Error {
30205
+ constructor(message) {
30206
+ super(message);
30207
+ this.name = "GuardrailError";
30208
+ }
30209
+ };
30210
+ var AUTHORITY_CLASSES = ["read", "pii", "mutate", "money", "recurring", "key", "webhook"];
30211
+ function isAuthorityClass(s) {
30212
+ return AUTHORITY_CLASSES.includes(s);
30213
+ }
30214
+ var cachedOptions = null;
30215
+ var destructiveTimestamps = [];
30216
+ var classTimestamps = /* @__PURE__ */ new Map();
30217
+ function readNumber(name, raw) {
30218
+ if (raw === void 0 || raw.trim() === "") return null;
30219
+ const n = Number(raw);
30220
+ if (!Number.isFinite(n) || n < 0) {
30221
+ throw new Error(`${name} must be a non-negative number (got ${JSON.stringify(raw)})`);
30222
+ }
30223
+ return n;
30224
+ }
30225
+ function parseDisabledClasses(raw) {
30226
+ if (!raw || raw.trim() === "") return null;
30227
+ const out = /* @__PURE__ */ new Set();
30228
+ for (const part of raw.split(",")) {
30229
+ const cls = part.trim();
30230
+ if (!cls) continue;
30231
+ if (!isAuthorityClass(cls)) {
30232
+ throw new Error(
30233
+ `LEMONSQUEEZY_DISABLE_CLASSES contains unknown class ${JSON.stringify(cls)} (expected one of: ${AUTHORITY_CLASSES.join(", ")})`
30234
+ );
30235
+ }
30236
+ out.add(cls);
30237
+ }
30238
+ return out.size > 0 ? out : null;
30239
+ }
30240
+ function parseClassRateLimits(raw) {
30241
+ if (!raw || raw.trim() === "") return null;
30242
+ const out = /* @__PURE__ */ new Map();
30243
+ for (const part of raw.split(",")) {
30244
+ const segment = part.trim();
30245
+ if (!segment) continue;
30246
+ const colon = segment.indexOf(":");
30247
+ if (colon < 0) {
30248
+ throw new Error(
30249
+ `LEMONSQUEEZY_RATE_LIMIT_PER_CLASS entry missing colon: ${JSON.stringify(segment)} (expected class:N or class:N/h)`
30250
+ );
30251
+ }
30252
+ const cls = segment.slice(0, colon).trim();
30253
+ if (!isAuthorityClass(cls)) {
30254
+ throw new Error(
30255
+ `LEMONSQUEEZY_RATE_LIMIT_PER_CLASS contains unknown class ${JSON.stringify(cls)} (expected one of: ${AUTHORITY_CLASSES.join(", ")})`
30256
+ );
30257
+ }
30258
+ const specRaw = segment.slice(colon + 1).trim();
30259
+ const slash = specRaw.indexOf("/");
30260
+ const numPart = slash >= 0 ? specRaw.slice(0, slash).trim() : specRaw;
30261
+ const unitPart = slash >= 0 ? specRaw.slice(slash + 1).trim().toLowerCase() : "m";
30262
+ const n = Number(numPart);
30263
+ if (!Number.isFinite(n) || n < 0 || numPart === "") {
30264
+ throw new Error(`LEMONSQUEEZY_RATE_LIMIT_PER_CLASS entry has invalid number: ${JSON.stringify(segment)}`);
30265
+ }
30266
+ let windowMs;
30267
+ if (unitPart === "m") windowMs = 6e4;
30268
+ else if (unitPart === "h") windowMs = 36e5;
30269
+ else {
30270
+ throw new Error(
30271
+ `LEMONSQUEEZY_RATE_LIMIT_PER_CLASS entry has invalid unit (expected m or h): ${JSON.stringify(segment)}`
30272
+ );
30273
+ }
30274
+ out.set(cls, { limit: n, windowMs });
30275
+ }
30276
+ return out.size > 0 ? out : null;
30277
+ }
30278
+ function loadOptions() {
30279
+ if (cachedOptions) return cachedOptions;
30280
+ const allowed = process.env.LEMONSQUEEZY_ALLOWED_STORE_IDS;
30281
+ const allowedSet = allowed ? new Set(
30282
+ allowed.split(",").map((s) => s.trim()).filter(Boolean)
30283
+ ) : null;
30284
+ cachedOptions = {
30285
+ allowedStoreIds: allowedSet && allowedSet.size > 0 ? allowedSet : null,
30286
+ maxRefundAmountCents: readNumber(
30287
+ "LEMONSQUEEZY_MAX_REFUND_AMOUNT_CENTS",
30288
+ process.env.LEMONSQUEEZY_MAX_REFUND_AMOUNT_CENTS
30289
+ ),
30290
+ rateLimitPerMinute: readNumber(
30291
+ "LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT",
30292
+ process.env.LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT
30293
+ ),
30294
+ disabledClasses: parseDisabledClasses(process.env.LEMONSQUEEZY_DISABLE_CLASSES),
30295
+ classRateLimits: parseClassRateLimits(process.env.LEMONSQUEEZY_RATE_LIMIT_PER_CLASS)
30296
+ };
30297
+ return cachedOptions;
30298
+ }
30299
+ function checkStoreAllowed(storeId) {
30300
+ if (!storeId) return;
30301
+ const o = loadOptions();
30302
+ if (!o.allowedStoreIds) return;
30303
+ if (!o.allowedStoreIds.has(String(storeId))) {
30304
+ throw new GuardrailError(`Store ID ${storeId} is not in LEMONSQUEEZY_ALLOWED_STORE_IDS allowlist`);
30305
+ }
30306
+ }
30307
+ function checkRefundAmount(cents) {
30308
+ const o = loadOptions();
30309
+ if (o.maxRefundAmountCents === null) return;
30310
+ if (cents > o.maxRefundAmountCents) {
30311
+ throw new GuardrailError(
30312
+ `Refund amount ${cents} cents exceeds LEMONSQUEEZY_MAX_REFUND_AMOUNT_CENTS (${o.maxRefundAmountCents})`
30313
+ );
30314
+ }
30315
+ }
30316
+ function checkDestructiveRateLimit(now = Date.now()) {
30317
+ const o = loadOptions();
30318
+ if (o.rateLimitPerMinute === null) return;
30319
+ const cutoff = now - 6e4;
30320
+ destructiveTimestamps = destructiveTimestamps.filter((t) => t > cutoff);
30321
+ if (destructiveTimestamps.length >= o.rateLimitPerMinute) {
30322
+ throw new GuardrailError(`Destructive call rate limit exceeded (${o.rateLimitPerMinute}/min). Wait and retry.`);
30323
+ }
30324
+ destructiveTimestamps.push(now);
30325
+ }
30326
+ function isStoreAllowlistActive() {
30327
+ return loadOptions().allowedStoreIds !== null;
30328
+ }
30329
+ function loadGuardrailOptions() {
30330
+ loadOptions();
30331
+ }
30332
+ function isPresent(value) {
30333
+ if (value === void 0 || value === null) return false;
30334
+ if (typeof value === "string" && value === "") return false;
30335
+ if (Array.isArray(value) && value.length === 0) return false;
30336
+ return true;
30337
+ }
30338
+ function checkStoreScopedToolInput(tool, input) {
30339
+ const toolAcceptsStoreId = "storeId" in tool.inputSchema.shape;
30340
+ if (toolAcceptsStoreId) {
30341
+ const raw = input.storeId;
30342
+ if (raw !== void 0 && raw !== null && raw !== "") {
30343
+ checkStoreAllowed(String(raw));
30344
+ } else if (isStoreAllowlistActive()) {
30345
+ throw new GuardrailError("storeId is required when LEMONSQUEEZY_ALLOWED_STORE_IDS is set");
30346
+ }
30347
+ }
30348
+ if (tool.requiredFilters && tool.requiredFilters.length > 0 && isStoreAllowlistActive()) {
30349
+ const anyPresent = tool.requiredFilters.some((key) => isPresent(input[key]));
30350
+ if (!anyPresent) {
30351
+ throw new GuardrailError(
30352
+ `At least one of [${tool.requiredFilters.join(", ")}] is required when LEMONSQUEEZY_ALLOWED_STORE_IDS is set`
30353
+ );
30354
+ }
30355
+ }
30356
+ }
30357
+ function isDestructiveCall(tool, input) {
30358
+ if (typeof tool.isDestructive === "function") return tool.isDestructive(input);
30359
+ return tool.annotations?.destructiveHint === true;
30360
+ }
30361
+ function checkClassAllowed(cls) {
30362
+ const o = loadOptions();
30363
+ if (!o.disabledClasses) return;
30364
+ if (o.disabledClasses.has(cls)) {
30365
+ throw new GuardrailError(`Tool authority class ${JSON.stringify(cls)} is disabled by LEMONSQUEEZY_DISABLE_CLASSES`);
30366
+ }
30367
+ }
30368
+ function checkClassRateLimit(cls, now = Date.now()) {
30369
+ const o = loadOptions();
30370
+ if (!o.classRateLimits) return;
30371
+ const spec = o.classRateLimits.get(cls);
30372
+ if (!spec) return;
30373
+ const cutoff = now - spec.windowMs;
30374
+ const list = (classTimestamps.get(cls) ?? []).filter((t) => t > cutoff);
30375
+ if (list.length >= spec.limit) {
30376
+ const unit = spec.windowMs === 6e4 ? "min" : spec.windowMs === 36e5 ? "hour" : `${spec.windowMs}ms`;
30377
+ classTimestamps.set(cls, list);
30378
+ throw new GuardrailError(
30379
+ `Class ${JSON.stringify(cls)} rate limit exceeded (${spec.limit}/${unit}). Wait and retry.`
30380
+ );
30381
+ }
30382
+ list.push(now);
30383
+ classTimestamps.set(cls, list);
30384
+ }
30385
+
30203
30386
  // src/logger.ts
30204
30387
  function getLogLevel() {
30205
30388
  const v = process.env.LEMONSQUEEZY_LOG;
@@ -31461,186 +31644,6 @@ var orderItemTools = [
31461
31644
  }
31462
31645
  ];
31463
31646
 
31464
- // src/guardrails.ts
31465
- var GuardrailError = class extends Error {
31466
- constructor(message) {
31467
- super(message);
31468
- this.name = "GuardrailError";
31469
- }
31470
- };
31471
- var AUTHORITY_CLASSES = ["read", "pii", "mutate", "money", "recurring", "key", "webhook"];
31472
- function isAuthorityClass(s) {
31473
- return AUTHORITY_CLASSES.includes(s);
31474
- }
31475
- var cachedOptions = null;
31476
- var destructiveTimestamps = [];
31477
- var classTimestamps = /* @__PURE__ */ new Map();
31478
- function readNumber(name, raw) {
31479
- if (raw === void 0 || raw.trim() === "") return null;
31480
- const n = Number(raw);
31481
- if (!Number.isFinite(n) || n < 0) {
31482
- throw new Error(`${name} must be a non-negative number (got ${JSON.stringify(raw)})`);
31483
- }
31484
- return n;
31485
- }
31486
- function parseDisabledClasses(raw) {
31487
- if (!raw || raw.trim() === "") return null;
31488
- const out = /* @__PURE__ */ new Set();
31489
- for (const part of raw.split(",")) {
31490
- const cls = part.trim();
31491
- if (!cls) continue;
31492
- if (!isAuthorityClass(cls)) {
31493
- throw new Error(
31494
- `LEMONSQUEEZY_DISABLE_CLASSES contains unknown class ${JSON.stringify(cls)} (expected one of: ${AUTHORITY_CLASSES.join(", ")})`
31495
- );
31496
- }
31497
- out.add(cls);
31498
- }
31499
- return out.size > 0 ? out : null;
31500
- }
31501
- function parseClassRateLimits(raw) {
31502
- if (!raw || raw.trim() === "") return null;
31503
- const out = /* @__PURE__ */ new Map();
31504
- for (const part of raw.split(",")) {
31505
- const segment = part.trim();
31506
- if (!segment) continue;
31507
- const colon = segment.indexOf(":");
31508
- if (colon < 0) {
31509
- throw new Error(
31510
- `LEMONSQUEEZY_RATE_LIMIT_PER_CLASS entry missing colon: ${JSON.stringify(segment)} (expected class:N or class:N/h)`
31511
- );
31512
- }
31513
- const cls = segment.slice(0, colon).trim();
31514
- if (!isAuthorityClass(cls)) {
31515
- throw new Error(
31516
- `LEMONSQUEEZY_RATE_LIMIT_PER_CLASS contains unknown class ${JSON.stringify(cls)} (expected one of: ${AUTHORITY_CLASSES.join(", ")})`
31517
- );
31518
- }
31519
- const specRaw = segment.slice(colon + 1).trim();
31520
- const slash = specRaw.indexOf("/");
31521
- const numPart = slash >= 0 ? specRaw.slice(0, slash).trim() : specRaw;
31522
- const unitPart = slash >= 0 ? specRaw.slice(slash + 1).trim().toLowerCase() : "m";
31523
- const n = Number(numPart);
31524
- if (!Number.isFinite(n) || n < 0 || numPart === "") {
31525
- throw new Error(`LEMONSQUEEZY_RATE_LIMIT_PER_CLASS entry has invalid number: ${JSON.stringify(segment)}`);
31526
- }
31527
- let windowMs;
31528
- if (unitPart === "m") windowMs = 6e4;
31529
- else if (unitPart === "h") windowMs = 36e5;
31530
- else {
31531
- throw new Error(
31532
- `LEMONSQUEEZY_RATE_LIMIT_PER_CLASS entry has invalid unit (expected m or h): ${JSON.stringify(segment)}`
31533
- );
31534
- }
31535
- out.set(cls, { limit: n, windowMs });
31536
- }
31537
- return out.size > 0 ? out : null;
31538
- }
31539
- function loadOptions() {
31540
- if (cachedOptions) return cachedOptions;
31541
- const allowed = process.env.LEMONSQUEEZY_ALLOWED_STORE_IDS;
31542
- const allowedSet = allowed ? new Set(
31543
- allowed.split(",").map((s) => s.trim()).filter(Boolean)
31544
- ) : null;
31545
- cachedOptions = {
31546
- allowedStoreIds: allowedSet && allowedSet.size > 0 ? allowedSet : null,
31547
- maxRefundAmountCents: readNumber(
31548
- "LEMONSQUEEZY_MAX_REFUND_AMOUNT_CENTS",
31549
- process.env.LEMONSQUEEZY_MAX_REFUND_AMOUNT_CENTS
31550
- ),
31551
- rateLimitPerMinute: readNumber(
31552
- "LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT",
31553
- process.env.LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT
31554
- ),
31555
- disabledClasses: parseDisabledClasses(process.env.LEMONSQUEEZY_DISABLE_CLASSES),
31556
- classRateLimits: parseClassRateLimits(process.env.LEMONSQUEEZY_RATE_LIMIT_PER_CLASS)
31557
- };
31558
- return cachedOptions;
31559
- }
31560
- function checkStoreAllowed(storeId) {
31561
- if (!storeId) return;
31562
- const o = loadOptions();
31563
- if (!o.allowedStoreIds) return;
31564
- if (!o.allowedStoreIds.has(String(storeId))) {
31565
- throw new GuardrailError(`Store ID ${storeId} is not in LEMONSQUEEZY_ALLOWED_STORE_IDS allowlist`);
31566
- }
31567
- }
31568
- function checkRefundAmount(cents) {
31569
- const o = loadOptions();
31570
- if (o.maxRefundAmountCents === null) return;
31571
- if (cents > o.maxRefundAmountCents) {
31572
- throw new GuardrailError(
31573
- `Refund amount ${cents} cents exceeds LEMONSQUEEZY_MAX_REFUND_AMOUNT_CENTS (${o.maxRefundAmountCents})`
31574
- );
31575
- }
31576
- }
31577
- function checkDestructiveRateLimit(now = Date.now()) {
31578
- const o = loadOptions();
31579
- if (o.rateLimitPerMinute === null) return;
31580
- const cutoff = now - 6e4;
31581
- destructiveTimestamps = destructiveTimestamps.filter((t) => t > cutoff);
31582
- if (destructiveTimestamps.length >= o.rateLimitPerMinute) {
31583
- throw new GuardrailError(`Destructive call rate limit exceeded (${o.rateLimitPerMinute}/min). Wait and retry.`);
31584
- }
31585
- destructiveTimestamps.push(now);
31586
- }
31587
- function isStoreAllowlistActive() {
31588
- return loadOptions().allowedStoreIds !== null;
31589
- }
31590
- function isPresent(value) {
31591
- if (value === void 0 || value === null) return false;
31592
- if (typeof value === "string" && value === "") return false;
31593
- if (Array.isArray(value) && value.length === 0) return false;
31594
- return true;
31595
- }
31596
- function checkStoreScopedToolInput(tool, input) {
31597
- const toolAcceptsStoreId = "storeId" in tool.inputSchema.shape;
31598
- if (toolAcceptsStoreId) {
31599
- const raw = input.storeId;
31600
- if (raw !== void 0 && raw !== null && raw !== "") {
31601
- checkStoreAllowed(String(raw));
31602
- } else if (isStoreAllowlistActive()) {
31603
- throw new GuardrailError("storeId is required when LEMONSQUEEZY_ALLOWED_STORE_IDS is set");
31604
- }
31605
- }
31606
- if (tool.requiredFilters && tool.requiredFilters.length > 0 && isStoreAllowlistActive()) {
31607
- const anyPresent = tool.requiredFilters.some((key) => isPresent(input[key]));
31608
- if (!anyPresent) {
31609
- throw new GuardrailError(
31610
- `At least one of [${tool.requiredFilters.join(", ")}] is required when LEMONSQUEEZY_ALLOWED_STORE_IDS is set`
31611
- );
31612
- }
31613
- }
31614
- }
31615
- function isDestructiveCall(tool, input) {
31616
- if (typeof tool.isDestructive === "function") return tool.isDestructive(input);
31617
- return tool.annotations?.destructiveHint === true;
31618
- }
31619
- function checkClassAllowed(cls) {
31620
- const o = loadOptions();
31621
- if (!o.disabledClasses) return;
31622
- if (o.disabledClasses.has(cls)) {
31623
- throw new GuardrailError(`Tool authority class ${JSON.stringify(cls)} is disabled by LEMONSQUEEZY_DISABLE_CLASSES`);
31624
- }
31625
- }
31626
- function checkClassRateLimit(cls, now = Date.now()) {
31627
- const o = loadOptions();
31628
- if (!o.classRateLimits) return;
31629
- const spec = o.classRateLimits.get(cls);
31630
- if (!spec) return;
31631
- const cutoff = now - spec.windowMs;
31632
- const list = (classTimestamps.get(cls) ?? []).filter((t) => t > cutoff);
31633
- if (list.length >= spec.limit) {
31634
- const unit = spec.windowMs === 6e4 ? "min" : spec.windowMs === 36e5 ? "hour" : `${spec.windowMs}ms`;
31635
- classTimestamps.set(cls, list);
31636
- throw new GuardrailError(
31637
- `Class ${JSON.stringify(cls)} rate limit exceeded (${spec.limit}/${unit}). Wait and retry.`
31638
- );
31639
- }
31640
- list.push(now);
31641
- classTimestamps.set(cls, list);
31642
- }
31643
-
31644
31647
  // src/tools/orders.ts
31645
31648
  var orderTools = [
31646
31649
  {
@@ -32776,12 +32779,13 @@ function readAuditLogResource(uri) {
32776
32779
  }
32777
32780
 
32778
32781
  // src/index.ts
32779
- var version2 = true ? "0.10.0" : (await null).createRequire(import.meta.url)("../package.json").version;
32782
+ var version2 = true ? "0.10.2" : (await null).createRequire(import.meta.url)("../package.json").version;
32780
32783
  var subcommand = process.argv[2];
32781
32784
  if (subcommand === "version" || subcommand === "--version") {
32782
32785
  console.log(version2);
32783
32786
  process.exit(0);
32784
32787
  }
32788
+ loadGuardrailOptions();
32785
32789
  var allTools = [
32786
32790
  ...userTools,
32787
32791
  ...storeTools,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yawlabs/lemonsqueezy-mcp",
3
- "version": "0.10.0",
3
+ "version": "0.10.2",
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",
@@ -25,7 +25,8 @@
25
25
  "lemonsqueezy-mcp": "dist/index.js"
26
26
  },
27
27
  "files": [
28
- "dist/index.js"
28
+ "dist/index.js",
29
+ "CHANGELOG.md"
29
30
  ],
30
31
  "scripts": {
31
32
  "build": "tsc && node build.mjs",