@yawlabs/lemonsqueezy-mcp 0.9.0 → 0.9.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/README.md +32 -13
  2. package/dist/index.js +389 -347
  3. package/package.json +3 -1
package/README.md CHANGED
@@ -62,7 +62,7 @@ docker build -t yawlabs/lemonsqueezy-mcp .
62
62
  docker run --rm -i -e LEMONSQUEEZY_API_KEY="your-api-key" yawlabs/lemonsqueezy-mcp
63
63
  ```
64
64
 
65
- A byte-identical `Containerfile` is also provided for Podman users.
65
+ A matching `Containerfile` is provided for Podman users. It is generated from `Dockerfile` via `npm run gen:containerfile`; CI calls `npm run check:containerfile` so a divergent edit fails review rather than drifting silently.
66
66
 
67
67
  ### Claude Code
68
68
 
@@ -208,9 +208,10 @@ Add to `claude_desktop_config.json`:
208
208
  - **Zero runtime dependencies** — Single bundled file for instant `npx` startup
209
209
  - **License API** — Activate, validate, and deactivate license keys without an API key
210
210
  - **MCP annotations** — Every tool declares read-only, destructive, and idempotent hints
211
- - **Retry with backoff** — 429 and 5xx retries (idempotent methods only) with exponential backoff and jitter
212
- - **Guardrails** — optional store allowlist, refund cap, and destructive-call rate limit
213
- - **Structured logging** — opt-in JSON logs to stderr for observability and audit
211
+ - **Retry with backoff** — 429 and 5xx retries (idempotent methods only) with exponential backoff, jitter, and a 90s overall-deadline ceiling
212
+ - **Guardrails** — opt-in store allowlist, refund cap, destructive-call rate limit, authority-class disable (`LEMONSQUEEZY_DISABLE_CLASSES`), and per-authority-class rate limits (`LEMONSQUEEZY_RATE_LIMIT_PER_CLASS`)
213
+ - **Audit log MCP Resource** — `lemonsqueezy://audit-log` exposes the last 1000 destructive-call entries as `application/x-ndjson` for clients without stderr access
214
+ - **Structured logging** — opt-in JSON logs to stderr with selectable levels (`error`, `audit`, `all`)
214
215
 
215
216
  ## Configuration
216
217
 
@@ -221,9 +222,9 @@ All configuration is via environment variables. Only `LEMONSQUEEZY_API_KEY` (or
221
222
  | `LEMONSQUEEZY_API_KEY` | LemonSqueezy API token. |
222
223
  | `LEMONSQUEEZY_API_KEY_COMMAND` | Command whose stdout produces the API key. Overrides `LEMONSQUEEZY_API_KEY`. Output is cached for 1 hour. Use this to pull short-lived credentials from a vault (`op read`, `gcloud secrets versions access`, etc.) without writing them to env vars. The cache is keyed by the command string, so changing it mid-process refreshes on the next request; it is also invalidated automatically on a 401/403 from the API, so a key rotated upstream takes effect on the next call without waiting for the TTL. |
223
224
  | `LEMONSQUEEZY_TEST_API_KEY` | Optional test-mode key. When set and non-empty, it takes precedence over `LEMONSQUEEZY_API_KEY` (but not over `LEMONSQUEEZY_API_KEY_COMMAND`). On first activation per process, the server prints a one-line JSON `test_mode` notice to stderr so you can confirm test mode is engaged. Use this to point the server at a sandbox/test store without unsetting your production key. |
224
- | `LEMONSQUEEZY_ALLOWED_STORE_IDS` | Comma-separated allowlist of store IDs. When set: (1) any tool whose input includes a `storeId` rejects calls to a non-allowed store; (2) tools that *accept* a `storeId` filter (e.g. `ls_list_orders`, `ls_list_subscriptions`) require it — calls without one are blocked so a missing filter cannot return data from every store the API key can see. Tools with no `storeId` field at all (e.g. `ls_refund_order`, `ls_cancel_subscription`, `ls_archive_customer`, `ls_delete_webhook`, `ls_delete_discount`, `ls_update_license_key`, `ls_list_stores`) route by their own resource ID and are **not** gated by this allowlist. For those, the only authoritative store boundary is the API key itself pair this setting with a LemonSqueezy API key scoped to the same store(s), and pair with `LEMONSQUEEZY_MAX_REFUND_AMOUNT_CENTS` / `LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT` for additional defense in depth. |
225
+ | `LEMONSQUEEZY_ALLOWED_STORE_IDS` | Comma-separated allowlist of store IDs. When set: (1) any tool whose input includes a `storeId` rejects calls to a non-allowed store; (2) tools that *accept* a `storeId` filter (e.g. `ls_list_orders`, `ls_list_subscriptions`) require it — calls without one are blocked so a missing filter cannot return data from every store the API key can see. Tools with no `storeId` field at all (e.g. `ls_refund_order`, `ls_cancel_subscription`, `ls_archive_customer`, `ls_delete_webhook`, `ls_delete_discount`, `ls_update_license_key`, `ls_list_stores`) route by their own resource ID and are **not** gated by this allowlist. LemonSqueezy API keys are issued at the account level and authorize access to every store in that account, so this allowlist is the only in-process store boundary the server can enforce. Pair it with `LEMONSQUEEZY_MAX_REFUND_AMOUNT_CENTS` / `LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT` / `LEMONSQUEEZY_RATE_LIMIT_PER_CLASS` for defense in depth, and — if your account hosts multiple stores you don't want exposed to the same agent — keep those stores under a separate LemonSqueezy account whose API key isn't reachable from this server. |
225
226
  | `LEMONSQUEEZY_MAX_REFUND_AMOUNT_CENTS` | Rejects `ls_refund_order` and `ls_refund_subscription_invoice` calls above this amount. |
226
- | `LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT` | Max destructive tool calls per 60-second rolling window. In-process limit — per MCP server instance, not global; each `npx` cold start resets the window. Counts include `ls_update_license_key` calls that set `disabled: true`, and `ls_update_subscription` calls that pause or switch plan. |
227
+ | `LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT` | Positive integer. Max destructive tool calls per 60-second rolling window. In-process limit — per MCP server instance, not global; each `npx` cold start resets the window. Counts include every refund, cancellation, archive, and delete tool, plus the input-dependent destructive paths: `ls_update_license_key` calls that set `disabled: true` *or* change `activationLimit`, `ls_update_subscription` calls that pause or switch plan, and `ls_update_customer` calls with `status: "archived"`. |
227
228
  | `LEMONSQUEEZY_DISABLE_CLASSES` | Comma-separated list of [authority classes](#authority-classes) to refuse outright. Any tool whose class is listed returns a `guardrail_block` before the API call is attempted. Example: `LEMONSQUEEZY_DISABLE_CLASSES=money,recurring,pii` lets an agent run reads but blocks refunds, subscription changes, and customer-record access. Unknown class names throw at server startup. |
228
229
  | `LEMONSQUEEZY_RATE_LIMIT_PER_CLASS` | Per-class rolling rate limits, comma-separated. Each entry is `class:N`, `class:N/m`, or `class:N/h` (bare numbers default to per-minute). Example: `money:2/h,recurring:5/h,key:10/m`. Composes with `LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT` — both must pass. In-process per server instance. |
229
230
  | `LEMONSQUEEZY_LOG` | Structured-log verbosity to stderr. Set to `all` (or legacy `json`) to log every tool and HTTP call, `audit` to log only destructive-call audit entries plus errors (recommended for production), `error` to log only failures. Unset: no logs. Destructive calls are tagged `audit: true` and include their inputs. |
@@ -238,7 +239,11 @@ HTTP errors include the upstream `X-Request-Id` when present, so support tickets
238
239
 
239
240
  ## Authority classes
240
241
 
241
- Every tool is tagged with an **authority class**a label for the kind of business authority a caller needs to invoke it. The class is separate from the binary destructive/read-only annotation: a customer-record read and a product list are both reads, but only one returns PII; a checkout creation and a refund are both writes, but only one moves money. Granular classes let an operator gate by the dimension that matters, instead of being stuck with a single "destructive" toggle.
242
+ **The strongest access control LemonSqueezy itself exposes is the API key boundary.** A LemonSqueezy API key authorizes its full account every store, every tool — and the only way to deny a class of authority through LemonSqueezy is to not give the API key to the agent in the first place. LemonSqueezy's team-membership UI scopes which humans can do which actions in the dashboard, but the public API key inherits the full authority of the account it was issued under; there's no "this key can read but not refund" toggle. So the authoritative boundary, as far as the upstream API is concerned, is *which API key the agent has*.
243
+
244
+ That means the env vars below are the primary in-process control surface for anything LemonSqueezy can't gate by itself — per-class rate ceilings (`RATE_LIMIT_PER_CLASS`), deploy-time class disables (`DISABLE_CLASSES`), refund caps (`MAX_REFUND_AMOUNT_CENTS`), and the audit log. They are belt-and-braces in the sense that an operator who can change the server's env can remove them; they are load-bearing in the sense that LemonSqueezy has no equivalent. If you need an agent that genuinely cannot reach a store or a class of action, the durable answer is a separate LemonSqueezy account whose key is never handed to that agent.
245
+
246
+ Every tool is tagged with an **authority class** — a label for the kind of business authority a caller needs to invoke it. The class is separate from the binary destructive/read-only annotation: a customer-record read and a product list are both reads, but only one returns PII; a checkout creation and a refund are both writes, but only one moves money.
242
247
 
243
248
  | Class | What it covers | Example tools |
244
249
  | --- | --- | --- |
@@ -250,9 +255,14 @@ Every tool is tagged with an **authority class** — a label for the kind of bus
250
255
  | `key` | License-key admin (activate, deactivate, disable, change activation limit). | `ls_update_license_key`, `ls_activate_license`, `ls_deactivate_license` |
251
256
  | `webhook` | Webhook configuration — affects the trust surface other systems rely on. | `ls_create_webhook`, `ls_update_webhook`, `ls_delete_webhook` |
252
257
 
253
- Note: `ls_get_order` returns customer fields incidentally, but its primary payload is the order — it stays in `read`, not `pii`. The class is reserved for tools whose *primary purpose* is the customer record. If you need to deny all access to customer-shaped data, prefer a scoped API key over class-disable alone.
258
+ Note: `ls_get_order` returns customer fields incidentally, but its primary payload is the order — it stays in `read`, not `pii`. The class is reserved for tools whose *primary purpose* is the customer record. If you need to deny all access to customer-shaped data, set `LEMONSQUEEZY_DISABLE_CLASSES=pii` and — if the agent must never under any circumstances touch that data — also issue its API key from a separate LemonSqueezy account that doesn't host customer records you care about.
259
+
260
+ The two opt-in env vars that consume this taxonomy:
254
261
 
255
- `LEMONSQUEEZY_DISABLE_CLASSES` blocks a class outright. `LEMONSQUEEZY_RATE_LIMIT_PER_CLASS` caps the call rate per class. Both are opt-in; with neither set, behavior is unchanged from prior versions.
262
+ - `LEMONSQUEEZY_RATE_LIMIT_PER_CLASS` caps the call rate per class. LemonSqueezy permissions cannot express "max 2 refunds per hour"; this is the only place that policy can live. **This is the load-bearing one for runaway-agent prevention.**
263
+ - `LEMONSQUEEZY_DISABLE_CLASSES` — blocks a class outright. Useful when fast deploy-time toggles matter — flipping `DISABLE_CLASSES=pii,mutate,money,recurring,key,webhook` to lock an analytics deployment into pure reads is a one-line config change. An operator who can change the server's env can also remove this gate, so for an authoritative deny use a separate LemonSqueezy account whose API key the agent never sees.
264
+
265
+ Both are opt-in; with neither set, behavior is unchanged from prior versions.
256
266
 
257
267
  ## Resources
258
268
 
@@ -260,16 +270,16 @@ The server exposes one MCP Resource for clients that prefer structural retrieval
260
270
 
261
271
  | URI | MIME type | Contents |
262
272
  | --- | --- | --- |
263
- | `lemonsqueezy://audit-log` | `application/x-ndjson` | The most recent destructive tool calls and outcomes (rate-limit blocks, refund-cap blocks, exceptions, successes). Bounded ring buffer of the last 1000 entries, most-recent-first, resets on server restart. Secret-shaped input fields are redacted before they reach the buffer. |
273
+ | `lemonsqueezy://audit-log` | `application/x-ndjson` | The most recent destructive tool calls and outcomes (rate-limit blocks, refund-cap blocks, exceptions, successes). Bounded ring buffer of the last 1000 entries, most-recent-first, resets on server restart. Redaction runs on the input payload before it reaches the buffer: any object key whose name matches a credential / PII pattern (`secret`, `password`, `token`, `api_key`, `bearer`, `authorization`, `signing_secret`, `private_key`, `pin`, `ssn`, `social_security_number`, `credit_card`, `card_number`, `cvv`, `cvc` — case-insensitive, whole-word) AND any string value matching the JWT bearer-token shape (`eyJ…`-prefixed, three base64url segments) is replaced with `[REDACTED]`. Ordinary identifiers (`licenseKey`, `instanceId`, `storeId`, `orderId`, `webhookId`) and UUID-shaped values are preserved. |
264
274
 
265
275
  ## Operating the server unattended
266
276
 
267
277
  For unattended/agentic use against a live store, we recommend:
268
278
 
269
- 1. Use a LemonSqueezy API key scoped to the specific store(s) the agent may touch — this is the only authoritative store boundary for tools that route by their own resource ID (refunds, cancels, archive, delete-webhook, etc.). Set `LEMONSQUEEZY_ALLOWED_STORE_IDS` to the same set as a defense-in-depth gate on the tools that *do* take a `storeId`.
279
+ 1. Issue an API key under a LemonSqueezy account that hosts only the store(s) the agent is allowed to touch — LemonSqueezy doesn't expose per-store API-key scoping, so account separation is the durable store boundary. Set `LEMONSQUEEZY_ALLOWED_STORE_IDS` to the same set as a belt-and-braces in-process gate on the tools that take a `storeId`.
270
280
  2. Set `LEMONSQUEEZY_MAX_REFUND_AMOUNT_CENTS` to a per-call cap well below any single-refund expectation.
271
281
  3. Set `LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT` to a small number (e.g. 5/min) as a runaway-agent circuit breaker. For finer control, add `LEMONSQUEEZY_RATE_LIMIT_PER_CLASS=money:2/h,recurring:5/h,key:10/m` so each [authority class](#authority-classes) has its own ceiling.
272
- 4. If a class shouldn't be reachable at all (e.g. an analytics agent that needs only `read`), set `LEMONSQUEEZY_DISABLE_CLASSES` to refuse the rest outright calls return a `guardrail_block` before the API is touched.
282
+ 4. If a class shouldn't be reachable at all (e.g. an analytics agent that needs only `read`), set `LEMONSQUEEZY_DISABLE_CLASSES` to the classes you want refused. The server rejects them before the API call is built. For an irrevocable deny, also issue the agent's API key from a separate LemonSqueezy account.
273
283
  5. Set `LEMONSQUEEZY_LOG=audit` and ship stderr to your log aggregator. The `audit` level keeps every destructive-call entry plus errors but drops successful reads so log volume stays bounded over weeks of operation. Alert on `status: "guardrail_block"` or elevated error rates per tool. Use `LEMONSQUEEZY_LOG=all` while debugging.
274
284
  6. Run `LEMONSQUEEZY_API_KEY_COMMAND` against a vault-backed secret so credentials can rotate without restarting the server process. The API client invalidates its in-process key cache automatically on a 401/403, so a rotated upstream key picks up on the next request rather than waiting on the 1h TTL.
275
285
 
@@ -290,6 +300,13 @@ npm test # full unit + handler suite
290
300
  npm run test:integration # requires LEMONSQUEEZY_TEST_API_KEY + LEMONSQUEEZY_TEST_STORE_ID
291
301
  ```
292
302
 
303
+ `Containerfile` is generated from `Dockerfile`. After editing `Dockerfile`:
304
+
305
+ ```bash
306
+ npm run gen:containerfile # regenerate Containerfile
307
+ npm run check:containerfile # CI runs this; non-zero exit means the two have drifted
308
+ ```
309
+
293
310
  ## Releasing
294
311
 
295
312
  Two paths from a clean checkout of `main`. Both produce the same artifact (npm publish with provenance + GitHub release).
@@ -313,7 +330,7 @@ git push origin main --follow-tags
313
330
  gh run list --limit 2
314
331
  ```
315
332
 
316
- The tag push triggers `.github/workflows/release.yml`, which runs `release.sh` in CI mode: lint, test, build, npm publish (with `--provenance`) using the org-level `NPM_TOKEN` secret, then GitHub release creation, then a smoke test against the published tarball. No local `npm login` needed.
333
+ The tag push triggers `.github/workflows/release.yml`, which runs `release.sh` in CI mode: lint, test, build, npm publish (with `--provenance`) using the org-level `NPM_TOKEN` secret, then GitHub release creation, then a smoke test against the published tarball, then a publish to the [Official MCP Registry](https://registry.modelcontextprotocol.io) via GitHub OIDC (no `MCP_*` secret needed; the namespace `io.github.YawLabs/*` is authorized purely from the OIDC `repository_owner` claim). No local `npm login` needed.
317
334
 
318
335
  ### 2. Local end-to-end
319
336
 
@@ -328,6 +345,8 @@ npm login --auth-type=web # publisher of @yawlabs/lemonsqueezy-mcp
328
345
  gh auth login # GitHub CLI for the release-creation step
329
346
  ```
330
347
 
348
+ The local path does **not** publish to the Official MCP Registry — that step lives only in CI and depends on a GitHub Actions OIDC token. To push a locally-released version to the registry, install [`mcp-publisher`](https://github.com/modelcontextprotocol/registry/releases), run `mcp-publisher login github` (interactive OAuth), then `mcp-publisher publish` from the repo root.
349
+
331
350
  ## License
332
351
 
333
352
  MIT
package/dist/index.js CHANGED
@@ -3133,20 +3133,20 @@ var require_utils = __commonJS({
3133
3133
  return acc;
3134
3134
  }
3135
3135
  var nonSimpleDomain = RegExp.prototype.test.bind(/[^!"$&'()*+,\-.;=_`a-z{}~]/u);
3136
- function consumeIsZone(buffer2) {
3137
- buffer2.length = 0;
3136
+ function consumeIsZone(buffer) {
3137
+ buffer.length = 0;
3138
3138
  return true;
3139
3139
  }
3140
- function consumeHextets(buffer2, address, output) {
3141
- if (buffer2.length) {
3142
- const hex3 = stringArrayToHexStripped(buffer2);
3140
+ function consumeHextets(buffer, address, output) {
3141
+ if (buffer.length) {
3142
+ const hex3 = stringArrayToHexStripped(buffer);
3143
3143
  if (hex3 !== "") {
3144
3144
  address.push(hex3);
3145
3145
  } else {
3146
3146
  output.error = true;
3147
3147
  return false;
3148
3148
  }
3149
- buffer2.length = 0;
3149
+ buffer.length = 0;
3150
3150
  }
3151
3151
  return true;
3152
3152
  }
@@ -3154,7 +3154,7 @@ var require_utils = __commonJS({
3154
3154
  let tokenCount = 0;
3155
3155
  const output = { error: false, address: "", zone: "" };
3156
3156
  const address = [];
3157
- const buffer2 = [];
3157
+ const buffer = [];
3158
3158
  let endipv6Encountered = false;
3159
3159
  let endIpv6 = false;
3160
3160
  let consume = consumeHextets;
@@ -3167,7 +3167,7 @@ var require_utils = __commonJS({
3167
3167
  if (endipv6Encountered === true) {
3168
3168
  endIpv6 = true;
3169
3169
  }
3170
- if (!consume(buffer2, address, output)) {
3170
+ if (!consume(buffer, address, output)) {
3171
3171
  break;
3172
3172
  }
3173
3173
  if (++tokenCount > 7) {
@@ -3180,22 +3180,22 @@ var require_utils = __commonJS({
3180
3180
  address.push(":");
3181
3181
  continue;
3182
3182
  } else if (cursor === "%") {
3183
- if (!consume(buffer2, address, output)) {
3183
+ if (!consume(buffer, address, output)) {
3184
3184
  break;
3185
3185
  }
3186
3186
  consume = consumeIsZone;
3187
3187
  } else {
3188
- buffer2.push(cursor);
3188
+ buffer.push(cursor);
3189
3189
  continue;
3190
3190
  }
3191
3191
  }
3192
- if (buffer2.length) {
3192
+ if (buffer.length) {
3193
3193
  if (consume === consumeIsZone) {
3194
- output.zone = buffer2.join("");
3194
+ output.zone = buffer.join("");
3195
3195
  } else if (endIpv6) {
3196
- address.push(buffer2.join(""));
3196
+ address.push(buffer.join(""));
3197
3197
  } else {
3198
- address.push(stringArrayToHexStripped(buffer2));
3198
+ address.push(stringArrayToHexStripped(buffer));
3199
3199
  }
3200
3200
  }
3201
3201
  output.address = address.join("");
@@ -10015,8 +10015,8 @@ var ZodSet = class _ZodSet extends ZodType {
10015
10015
  maxSize: { value: maxSize, message: errorUtil.toString(message) }
10016
10016
  });
10017
10017
  }
10018
- size(size, message) {
10019
- return this.min(size, message).max(size, message);
10018
+ size(size2, message) {
10019
+ return this.min(size2, message).max(size2, message);
10020
10020
  }
10021
10021
  nonempty(message) {
10022
10022
  return this.min(1, message);
@@ -12392,8 +12392,8 @@ var $ZodCheckMaxSize = /* @__PURE__ */ $constructor("$ZodCheckMaxSize", (inst, d
12392
12392
  });
12393
12393
  inst._zod.check = (payload) => {
12394
12394
  const input = payload.value;
12395
- const size = input.size;
12396
- if (size <= def.maximum)
12395
+ const size2 = input.size;
12396
+ if (size2 <= def.maximum)
12397
12397
  return;
12398
12398
  payload.issues.push({
12399
12399
  origin: getSizableOrigin(input),
@@ -12420,8 +12420,8 @@ var $ZodCheckMinSize = /* @__PURE__ */ $constructor("$ZodCheckMinSize", (inst, d
12420
12420
  });
12421
12421
  inst._zod.check = (payload) => {
12422
12422
  const input = payload.value;
12423
- const size = input.size;
12424
- if (size >= def.minimum)
12423
+ const size2 = input.size;
12424
+ if (size2 >= def.minimum)
12425
12425
  return;
12426
12426
  payload.issues.push({
12427
12427
  origin: getSizableOrigin(input),
@@ -12449,10 +12449,10 @@ var $ZodCheckSizeEquals = /* @__PURE__ */ $constructor("$ZodCheckSizeEquals", (i
12449
12449
  });
12450
12450
  inst._zod.check = (payload) => {
12451
12451
  const input = payload.value;
12452
- const size = input.size;
12453
- if (size === def.size)
12452
+ const size2 = input.size;
12453
+ if (size2 === def.size)
12454
12454
  return;
12455
- const tooBig = size > def.size;
12455
+ const tooBig = size2 > def.size;
12456
12456
  payload.issues.push({
12457
12457
  origin: getSizableOrigin(input),
12458
12458
  ...tooBig ? { code: "too_big", maximum: def.size } : { code: "too_small", minimum: def.size },
@@ -14484,12 +14484,12 @@ var $ZodPipe = /* @__PURE__ */ $constructor("$ZodPipe", (inst, def) => {
14484
14484
  return handlePipeResult(left, def.out, ctx);
14485
14485
  };
14486
14486
  });
14487
- function handlePipeResult(left, next, ctx) {
14487
+ function handlePipeResult(left, next2, ctx) {
14488
14488
  if (left.issues.length) {
14489
14489
  left.aborted = true;
14490
14490
  return left;
14491
14491
  }
14492
- return next._zod.run({ value: left.value, issues: left.issues }, ctx);
14492
+ return next2._zod.run({ value: left.value, issues: left.issues }, ctx);
14493
14493
  }
14494
14494
  var $ZodCodec = /* @__PURE__ */ $constructor("$ZodCodec", (inst, def) => {
14495
14495
  $ZodType.init(inst, def);
@@ -20922,11 +20922,11 @@ function _minSize(minimum, params) {
20922
20922
  });
20923
20923
  }
20924
20924
  // @__NO_SIDE_EFFECTS__
20925
- function _size(size, params) {
20925
+ function _size(size2, params) {
20926
20926
  return new $ZodCheckSizeEquals({
20927
20927
  check: "size_equals",
20928
20928
  ...normalizeParams(params),
20929
- size
20929
+ size: size2
20930
20930
  });
20931
20931
  }
20932
20932
  // @__NO_SIDE_EFFECTS__
@@ -23878,8 +23878,8 @@ var ZodFile = /* @__PURE__ */ $constructor("ZodFile", (inst, def) => {
23878
23878
  $ZodFile.init(inst, def);
23879
23879
  ZodType2.init(inst, def);
23880
23880
  inst._zod.processJSONSchema = (ctx, json2, params) => fileProcessor(inst, ctx, json2, params);
23881
- inst.min = (size, params) => inst.check(_minSize(size, params));
23882
- inst.max = (size, params) => inst.check(_maxSize(size, params));
23881
+ inst.min = (size2, params) => inst.check(_minSize(size2, params));
23882
+ inst.max = (size2, params) => inst.check(_maxSize(size2, params));
23883
23883
  inst.mime = (types, params) => inst.check(_mime(Array.isArray(types) ? types : [types], params));
23884
23884
  });
23885
23885
  function file(params) {
@@ -30200,203 +30200,6 @@ var StdioServerTransport = class {
30200
30200
  }
30201
30201
  };
30202
30202
 
30203
- // src/audit-buffer.ts
30204
- var DEFAULT_CAP = 1e3;
30205
- var buffer = [];
30206
- var cap = DEFAULT_CAP;
30207
- function pushAuditEntry(entry) {
30208
- buffer.push(entry);
30209
- if (buffer.length > cap) {
30210
- buffer.splice(0, buffer.length - cap);
30211
- }
30212
- }
30213
- function readAuditEntries(limit) {
30214
- const reversed = buffer.slice().reverse();
30215
- if (limit === void 0 || limit >= reversed.length) return reversed;
30216
- if (limit <= 0) return [];
30217
- return reversed.slice(0, limit);
30218
- }
30219
-
30220
- // src/guardrails.ts
30221
- var GuardrailError = class extends Error {
30222
- constructor(message) {
30223
- super(message);
30224
- this.name = "GuardrailError";
30225
- }
30226
- };
30227
- var AUTHORITY_CLASSES = ["read", "pii", "mutate", "money", "recurring", "key", "webhook"];
30228
- function isAuthorityClass(s) {
30229
- return AUTHORITY_CLASSES.includes(s);
30230
- }
30231
- var cachedOptions = null;
30232
- var destructiveTimestamps = [];
30233
- var classTimestamps = /* @__PURE__ */ new Map();
30234
- function readNumber(name, raw) {
30235
- if (raw === void 0 || raw.trim() === "") return null;
30236
- const n = Number(raw);
30237
- if (!Number.isFinite(n) || n < 0) {
30238
- throw new Error(`${name} must be a non-negative number (got ${JSON.stringify(raw)})`);
30239
- }
30240
- return n;
30241
- }
30242
- function parseDisabledClasses(raw) {
30243
- if (!raw || raw.trim() === "") return null;
30244
- const out = /* @__PURE__ */ new Set();
30245
- for (const part of raw.split(",")) {
30246
- const cls = part.trim();
30247
- if (!cls) continue;
30248
- if (!isAuthorityClass(cls)) {
30249
- throw new Error(
30250
- `LEMONSQUEEZY_DISABLE_CLASSES contains unknown class ${JSON.stringify(cls)} (expected one of: ${AUTHORITY_CLASSES.join(", ")})`
30251
- );
30252
- }
30253
- out.add(cls);
30254
- }
30255
- return out.size > 0 ? out : null;
30256
- }
30257
- function parseClassRateLimits(raw) {
30258
- if (!raw || raw.trim() === "") return null;
30259
- const out = /* @__PURE__ */ new Map();
30260
- for (const part of raw.split(",")) {
30261
- const segment = part.trim();
30262
- if (!segment) continue;
30263
- const colon = segment.indexOf(":");
30264
- if (colon < 0) {
30265
- throw new Error(
30266
- `LEMONSQUEEZY_RATE_LIMIT_PER_CLASS entry missing colon: ${JSON.stringify(segment)} (expected class:N or class:N/h)`
30267
- );
30268
- }
30269
- const cls = segment.slice(0, colon).trim();
30270
- if (!isAuthorityClass(cls)) {
30271
- throw new Error(
30272
- `LEMONSQUEEZY_RATE_LIMIT_PER_CLASS contains unknown class ${JSON.stringify(cls)} (expected one of: ${AUTHORITY_CLASSES.join(", ")})`
30273
- );
30274
- }
30275
- const specRaw = segment.slice(colon + 1).trim();
30276
- const slash = specRaw.indexOf("/");
30277
- const numPart = slash >= 0 ? specRaw.slice(0, slash).trim() : specRaw;
30278
- const unitPart = slash >= 0 ? specRaw.slice(slash + 1).trim().toLowerCase() : "m";
30279
- const n = Number(numPart);
30280
- if (!Number.isFinite(n) || n < 0 || numPart === "") {
30281
- throw new Error(`LEMONSQUEEZY_RATE_LIMIT_PER_CLASS entry has invalid number: ${JSON.stringify(segment)}`);
30282
- }
30283
- let windowMs;
30284
- if (unitPart === "m") windowMs = 6e4;
30285
- else if (unitPart === "h") windowMs = 36e5;
30286
- else {
30287
- throw new Error(
30288
- `LEMONSQUEEZY_RATE_LIMIT_PER_CLASS entry has invalid unit (expected m or h): ${JSON.stringify(segment)}`
30289
- );
30290
- }
30291
- out.set(cls, { limit: n, windowMs });
30292
- }
30293
- return out.size > 0 ? out : null;
30294
- }
30295
- function loadOptions() {
30296
- if (cachedOptions) return cachedOptions;
30297
- const allowed = process.env.LEMONSQUEEZY_ALLOWED_STORE_IDS;
30298
- const allowedSet = allowed ? new Set(
30299
- allowed.split(",").map((s) => s.trim()).filter(Boolean)
30300
- ) : null;
30301
- cachedOptions = {
30302
- allowedStoreIds: allowedSet && allowedSet.size > 0 ? allowedSet : null,
30303
- maxRefundAmountCents: readNumber(
30304
- "LEMONSQUEEZY_MAX_REFUND_AMOUNT_CENTS",
30305
- process.env.LEMONSQUEEZY_MAX_REFUND_AMOUNT_CENTS
30306
- ),
30307
- rateLimitPerMinute: readNumber(
30308
- "LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT",
30309
- process.env.LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT
30310
- ),
30311
- disabledClasses: parseDisabledClasses(process.env.LEMONSQUEEZY_DISABLE_CLASSES),
30312
- classRateLimits: parseClassRateLimits(process.env.LEMONSQUEEZY_RATE_LIMIT_PER_CLASS)
30313
- };
30314
- return cachedOptions;
30315
- }
30316
- function checkStoreAllowed(storeId) {
30317
- if (!storeId) return;
30318
- const o = loadOptions();
30319
- if (!o.allowedStoreIds) return;
30320
- if (!o.allowedStoreIds.has(String(storeId))) {
30321
- throw new GuardrailError(`Store ID ${storeId} is not in LEMONSQUEEZY_ALLOWED_STORE_IDS allowlist`);
30322
- }
30323
- }
30324
- function checkRefundAmount(cents) {
30325
- const o = loadOptions();
30326
- if (o.maxRefundAmountCents === null) return;
30327
- if (cents > o.maxRefundAmountCents) {
30328
- throw new GuardrailError(
30329
- `Refund amount ${cents} cents exceeds LEMONSQUEEZY_MAX_REFUND_AMOUNT_CENTS (${o.maxRefundAmountCents})`
30330
- );
30331
- }
30332
- }
30333
- function checkDestructiveRateLimit(now = Date.now()) {
30334
- const o = loadOptions();
30335
- if (o.rateLimitPerMinute === null) return;
30336
- const cutoff = now - 6e4;
30337
- destructiveTimestamps = destructiveTimestamps.filter((t) => t > cutoff);
30338
- if (destructiveTimestamps.length >= o.rateLimitPerMinute) {
30339
- throw new GuardrailError(`Destructive call rate limit exceeded (${o.rateLimitPerMinute}/min). Wait and retry.`);
30340
- }
30341
- destructiveTimestamps.push(now);
30342
- }
30343
- function isStoreAllowlistActive() {
30344
- return loadOptions().allowedStoreIds !== null;
30345
- }
30346
- function isPresent(value) {
30347
- if (value === void 0 || value === null) return false;
30348
- if (typeof value === "string" && value === "") return false;
30349
- if (Array.isArray(value) && value.length === 0) return false;
30350
- return true;
30351
- }
30352
- function checkStoreScopedToolInput(tool, input) {
30353
- const toolAcceptsStoreId = "storeId" in tool.inputSchema.shape;
30354
- if (toolAcceptsStoreId) {
30355
- const raw = input.storeId;
30356
- if (raw !== void 0 && raw !== null && raw !== "") {
30357
- checkStoreAllowed(String(raw));
30358
- } else if (isStoreAllowlistActive()) {
30359
- throw new GuardrailError("storeId is required when LEMONSQUEEZY_ALLOWED_STORE_IDS is set");
30360
- }
30361
- }
30362
- if (tool.requiredFilters && tool.requiredFilters.length > 0 && isStoreAllowlistActive()) {
30363
- const anyPresent = tool.requiredFilters.some((key) => isPresent(input[key]));
30364
- if (!anyPresent) {
30365
- throw new GuardrailError(
30366
- `At least one of [${tool.requiredFilters.join(", ")}] is required when LEMONSQUEEZY_ALLOWED_STORE_IDS is set`
30367
- );
30368
- }
30369
- }
30370
- }
30371
- function isDestructiveCall(tool, input) {
30372
- if (typeof tool.isDestructive === "function") return tool.isDestructive(input);
30373
- return tool.annotations?.destructiveHint === true;
30374
- }
30375
- function checkClassAllowed(cls) {
30376
- const o = loadOptions();
30377
- if (!o.disabledClasses) return;
30378
- if (o.disabledClasses.has(cls)) {
30379
- throw new GuardrailError(`Tool authority class ${JSON.stringify(cls)} is disabled by LEMONSQUEEZY_DISABLE_CLASSES`);
30380
- }
30381
- }
30382
- function checkClassRateLimit(cls, now = Date.now()) {
30383
- const o = loadOptions();
30384
- if (!o.classRateLimits) return;
30385
- const spec = o.classRateLimits.get(cls);
30386
- if (!spec) return;
30387
- const cutoff = now - spec.windowMs;
30388
- const list = (classTimestamps.get(cls) ?? []).filter((t) => t > cutoff);
30389
- if (list.length >= spec.limit) {
30390
- const unit = spec.windowMs === 6e4 ? "min" : spec.windowMs === 36e5 ? "hour" : `${spec.windowMs}ms`;
30391
- classTimestamps.set(cls, list);
30392
- throw new GuardrailError(
30393
- `Class ${JSON.stringify(cls)} rate limit exceeded (${spec.limit}/${unit}). Wait and retry.`
30394
- );
30395
- }
30396
- list.push(now);
30397
- classTimestamps.set(cls, list);
30398
- }
30399
-
30400
30203
  // src/logger.ts
30401
30204
  function getLogLevel() {
30402
30205
  const v = process.env.LEMONSQUEEZY_LOG;
@@ -30424,6 +30227,15 @@ function shouldLog(entry, level) {
30424
30227
  return isErrorEntry(entry);
30425
30228
  }
30426
30229
  }
30230
+ function safeString(value) {
30231
+ if (value === void 0) return void 0;
30232
+ if (typeof value === "string") return value;
30233
+ try {
30234
+ return String(value);
30235
+ } catch {
30236
+ return "[unrepresentable]";
30237
+ }
30238
+ }
30427
30239
  function logEvent(entry) {
30428
30240
  const level = getLogLevel();
30429
30241
  if (!shouldLog(entry, level)) return;
@@ -30445,7 +30257,7 @@ function logEvent(entry) {
30445
30257
  latency_ms: entry.latency_ms,
30446
30258
  request_id: entry.request_id,
30447
30259
  audit: entry.audit,
30448
- error: entry.error,
30260
+ error: safeString(entry.error),
30449
30261
  log_error: "inputs_not_serializable"
30450
30262
  });
30451
30263
  process.stderr.write(`${fallback}
@@ -30454,41 +30266,6 @@ function logEvent(entry) {
30454
30266
  }
30455
30267
  }
30456
30268
 
30457
- // src/redact.ts
30458
- var SECRET_KEY_RE = /^(secret|password|token|api[_-]?key|bearer|authorization|signing[_-]?secret)$/i;
30459
- var REDACTED = "[REDACTED]";
30460
- var CIRCULAR = "[CIRCULAR]";
30461
- var MAX_DEPTH = 32;
30462
- function isPlainObject3(value) {
30463
- if (value === null || typeof value !== "object") return false;
30464
- const proto = Object.getPrototypeOf(value);
30465
- return proto === Object.prototype || proto === null;
30466
- }
30467
- function redactInner(value, visited, depth) {
30468
- if (depth > MAX_DEPTH) return CIRCULAR;
30469
- if (value === null || typeof value !== "object") return value;
30470
- if (Array.isArray(value)) {
30471
- if (visited.has(value)) return CIRCULAR;
30472
- visited.add(value);
30473
- return value.map((item) => redactInner(item, visited, depth + 1));
30474
- }
30475
- if (!isPlainObject3(value)) return value;
30476
- if (visited.has(value)) return CIRCULAR;
30477
- visited.add(value);
30478
- const out = {};
30479
- for (const [key, val] of Object.entries(value)) {
30480
- if (SECRET_KEY_RE.test(key)) {
30481
- out[key] = REDACTED;
30482
- } else {
30483
- out[key] = redactInner(val, visited, depth + 1);
30484
- }
30485
- }
30486
- return out;
30487
- }
30488
- function redactSecrets(input) {
30489
- return redactInner(input, /* @__PURE__ */ new WeakSet(), 0);
30490
- }
30491
-
30492
30269
  // src/retry.ts
30493
30270
  var REQUEST_TIMEOUT_MS = 3e4;
30494
30271
  var DEFAULT_RETRY_WAIT_MS = 1e3;
@@ -30595,11 +30372,15 @@ async function fetchWithRetry(url2, init, opts) {
30595
30372
 
30596
30373
  // src/secret.ts
30597
30374
  import { execFile } from "node:child_process";
30375
+ import { createHash } from "node:crypto";
30598
30376
  import { promisify } from "node:util";
30599
30377
  var execFileAsync = promisify(execFile);
30600
30378
  var CACHE_TTL_MS = 60 * 60 * 1e3;
30601
30379
  var COMMAND_TIMEOUT_MS = 1e4;
30602
30380
  var COMMAND_MAX_BUFFER = 64 * 1024;
30381
+ function fingerprintFor(mode, raw) {
30382
+ return createHash("sha256").update(`${mode}:${raw}`).digest("hex");
30383
+ }
30603
30384
  var cached2 = null;
30604
30385
  var testModeAnnounced = false;
30605
30386
  function announceTestModeOnce() {
@@ -30652,7 +30433,7 @@ function intoCache(fingerprint, key) {
30652
30433
  async function loadApiKey() {
30653
30434
  const cmdStr = process.env.LEMONSQUEEZY_API_KEY_COMMAND;
30654
30435
  if (cmdStr && cmdStr.trim() !== "") {
30655
- const fingerprint2 = `cmd:${cmdStr}`;
30436
+ const fingerprint2 = fingerprintFor("cmd", cmdStr);
30656
30437
  const hit2 = fromCache(fingerprint2);
30657
30438
  if (hit2 !== null) return hit2;
30658
30439
  const { command, args } = parseCommand(cmdStr);
@@ -30674,7 +30455,7 @@ async function loadApiKey() {
30674
30455
  }
30675
30456
  const testRaw = process.env.LEMONSQUEEZY_TEST_API_KEY;
30676
30457
  if (testRaw && testRaw.trim() !== "") {
30677
- const fingerprint2 = `test:${testRaw}`;
30458
+ const fingerprint2 = fingerprintFor("test", testRaw);
30678
30459
  const hit2 = fromCache(fingerprint2);
30679
30460
  if (hit2 !== null) {
30680
30461
  announceTestModeOnce();
@@ -30691,7 +30472,7 @@ async function loadApiKey() {
30691
30472
  if (raw.trim() === "") {
30692
30473
  throw new Error("LEMONSQUEEZY_API_KEY is set but empty. Provide a valid API key.");
30693
30474
  }
30694
- const fingerprint = `env:${raw}`;
30475
+ const fingerprint = fingerprintFor("env", raw);
30695
30476
  const hit = fromCache(fingerprint);
30696
30477
  if (hit !== null) return hit;
30697
30478
  intoCache(fingerprint, raw);
@@ -31680,6 +31461,186 @@ var orderItemTools = [
31680
31461
  }
31681
31462
  ];
31682
31463
 
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
+
31683
31644
  // src/tools/orders.ts
31684
31645
  var orderTools = [
31685
31646
  {
@@ -32495,8 +32456,168 @@ var webhookTools = [
32495
32456
  }
32496
32457
  ];
32497
32458
 
32459
+ // src/audit-buffer.ts
32460
+ var DEFAULT_CAP = 1e3;
32461
+ var backing = new Array(DEFAULT_CAP);
32462
+ var cap = DEFAULT_CAP;
32463
+ var next = 0;
32464
+ var size = 0;
32465
+ function pushAuditEntry(entry) {
32466
+ backing[next] = entry;
32467
+ next = (next + 1) % cap;
32468
+ if (size < cap) size += 1;
32469
+ }
32470
+ function readAuditEntries(limit) {
32471
+ if (limit !== void 0 && limit <= 0) return [];
32472
+ const wanted = limit === void 0 ? size : Math.min(limit, size);
32473
+ if (wanted === 0) return [];
32474
+ const out = new Array(wanted);
32475
+ let cursor = (next - 1 + cap) % cap;
32476
+ for (let i = 0; i < wanted; i++) {
32477
+ out[i] = backing[cursor];
32478
+ cursor = (cursor - 1 + cap) % cap;
32479
+ }
32480
+ return out;
32481
+ }
32482
+
32483
+ // src/redact.ts
32484
+ var SECRET_KEY_RE = /^(secret|password|token|api[_-]?key|bearer|authorization|signing[_-]?secret|private[_-]?key|pin|ssn|social[_-]?security[_-]?number|credit[_-]?card|card[_-]?number|cvv|cvc)$/i;
32485
+ var JWT_VALUE_RE = /^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}$/;
32486
+ var REDACTED = "[REDACTED]";
32487
+ var CIRCULAR = "[CIRCULAR]";
32488
+ var MAX_DEPTH = 32;
32489
+ function isPlainObject3(value) {
32490
+ if (value === null || typeof value !== "object") return false;
32491
+ const proto = Object.getPrototypeOf(value);
32492
+ return proto === Object.prototype || proto === null;
32493
+ }
32494
+ function looksLikeBearerToken(value) {
32495
+ if (typeof value !== "string") return false;
32496
+ if (value.length < 20) return false;
32497
+ return JWT_VALUE_RE.test(value);
32498
+ }
32499
+ function redactInner(value, visited, depth) {
32500
+ if (depth > MAX_DEPTH) return CIRCULAR;
32501
+ if (typeof value === "string" && looksLikeBearerToken(value)) return REDACTED;
32502
+ if (value === null || typeof value !== "object") return value;
32503
+ if (Array.isArray(value)) {
32504
+ if (visited.has(value)) return CIRCULAR;
32505
+ visited.add(value);
32506
+ return value.map((item) => redactInner(item, visited, depth + 1));
32507
+ }
32508
+ if (!isPlainObject3(value)) return value;
32509
+ if (visited.has(value)) return CIRCULAR;
32510
+ visited.add(value);
32511
+ const out = {};
32512
+ for (const [key, val] of Object.entries(value)) {
32513
+ if (SECRET_KEY_RE.test(key)) {
32514
+ out[key] = REDACTED;
32515
+ } else {
32516
+ out[key] = redactInner(val, visited, depth + 1);
32517
+ }
32518
+ }
32519
+ return out;
32520
+ }
32521
+ function redactSecrets(input) {
32522
+ return redactInner(input, /* @__PURE__ */ new WeakSet(), 0);
32523
+ }
32524
+
32525
+ // src/wrapper.ts
32526
+ function createToolHandler(tool) {
32527
+ return async (input) => {
32528
+ if (input === null || typeof input !== "object") {
32529
+ const contractError = `createToolHandler expected an object input for tool "${tool.name}", got ${input === null ? "null" : typeof input}. The MCP SDK should have rejected this before reaching the handler.`;
32530
+ logEvent({
32531
+ event: "tool_call",
32532
+ tool: tool.name,
32533
+ status: "exception",
32534
+ latency_ms: 0,
32535
+ error: contractError
32536
+ });
32537
+ return {
32538
+ content: [{ type: "text", text: `Error: ${contractError}` }],
32539
+ isError: true
32540
+ };
32541
+ }
32542
+ const inputAsRecord = input;
32543
+ const toolAsRecord = tool;
32544
+ const isDestructive = isDestructiveCall(toolAsRecord, inputAsRecord);
32545
+ const start = Date.now();
32546
+ try {
32547
+ checkClassAllowed(tool.authorityClass);
32548
+ checkClassRateLimit(tool.authorityClass);
32549
+ if (isDestructive) checkDestructiveRateLimit();
32550
+ checkStoreScopedToolInput(toolAsRecord, inputAsRecord);
32551
+ const response = await tool.handler(input);
32552
+ const latency_ms = Date.now() - start;
32553
+ const successEntry = {
32554
+ event: "tool_call",
32555
+ tool: tool.name,
32556
+ status: response.ok ? "ok" : "error",
32557
+ latency_ms,
32558
+ request_id: response.requestId,
32559
+ error: response.ok ? void 0 : response.error,
32560
+ audit: isDestructive ? true : void 0,
32561
+ inputs: isDestructive ? redactSecrets(input) : void 0
32562
+ };
32563
+ logEvent(successEntry);
32564
+ if (isDestructive) {
32565
+ pushAuditEntry({ ts: (/* @__PURE__ */ new Date()).toISOString(), ...successEntry });
32566
+ }
32567
+ if (!response.ok) {
32568
+ return {
32569
+ content: [
32570
+ {
32571
+ type: "text",
32572
+ text: `Error: ${response.error || "Unknown error"}`
32573
+ }
32574
+ ],
32575
+ isError: true
32576
+ };
32577
+ }
32578
+ const text = JSON.stringify(response.data ?? { success: true }, null, 2);
32579
+ return {
32580
+ content: [{ type: "text", text }]
32581
+ };
32582
+ } catch (err) {
32583
+ const message = err instanceof Error ? err.message : String(err);
32584
+ const latency_ms = Date.now() - start;
32585
+ const errorEntry = {
32586
+ event: "tool_call",
32587
+ tool: tool.name,
32588
+ status: err instanceof GuardrailError ? "guardrail_block" : "exception",
32589
+ latency_ms,
32590
+ error: message,
32591
+ audit: isDestructive ? true : void 0,
32592
+ inputs: isDestructive ? redactSecrets(input) : void 0
32593
+ };
32594
+ logEvent(errorEntry);
32595
+ if (isDestructive) {
32596
+ pushAuditEntry({ ts: (/* @__PURE__ */ new Date()).toISOString(), ...errorEntry });
32597
+ }
32598
+ return {
32599
+ content: [{ type: "text", text: `Error: ${message}` }],
32600
+ isError: true
32601
+ };
32602
+ }
32603
+ };
32604
+ }
32605
+ function readAuditLogResource(uri) {
32606
+ const entries = readAuditEntries();
32607
+ const text = entries.map((e) => JSON.stringify(e)).join("\n");
32608
+ return {
32609
+ contents: [
32610
+ {
32611
+ uri: uri.href,
32612
+ mimeType: "application/x-ndjson",
32613
+ text
32614
+ }
32615
+ ]
32616
+ };
32617
+ }
32618
+
32498
32619
  // src/index.ts
32499
- var version2 = true ? "0.9.0" : (await null).createRequire(import.meta.url)("../package.json").version;
32620
+ var version2 = true ? "0.9.2" : (await null).createRequire(import.meta.url)("../package.json").version;
32500
32621
  var subcommand = process.argv[2];
32501
32622
  if (subcommand === "version" || subcommand === "--version") {
32502
32623
  console.log(version2);
@@ -32530,74 +32651,7 @@ var server = new McpServer({
32530
32651
  version: version2
32531
32652
  });
32532
32653
  for (const tool of allTools) {
32533
- server.tool(
32534
- tool.name,
32535
- tool.description,
32536
- tool.inputSchema.shape,
32537
- tool.annotations,
32538
- async (input) => {
32539
- const isDestructive = isDestructiveCall(tool, input);
32540
- const start = Date.now();
32541
- try {
32542
- checkClassAllowed(tool.authorityClass);
32543
- checkClassRateLimit(tool.authorityClass);
32544
- if (isDestructive) checkDestructiveRateLimit();
32545
- checkStoreScopedToolInput(tool, input);
32546
- const result = await tool.handler(input);
32547
- const response = result;
32548
- const latency_ms = Date.now() - start;
32549
- const successEntry = {
32550
- event: "tool_call",
32551
- tool: tool.name,
32552
- status: response.ok ? "ok" : "error",
32553
- latency_ms,
32554
- request_id: response.requestId,
32555
- error: response.ok ? void 0 : response.error,
32556
- audit: isDestructive ? true : void 0,
32557
- inputs: isDestructive ? redactSecrets(input) : void 0
32558
- };
32559
- logEvent(successEntry);
32560
- if (isDestructive) {
32561
- pushAuditEntry({ ts: (/* @__PURE__ */ new Date()).toISOString(), ...successEntry });
32562
- }
32563
- if (!response.ok) {
32564
- return {
32565
- content: [
32566
- {
32567
- type: "text",
32568
- text: `Error: ${response.error || "Unknown error"}`
32569
- }
32570
- ],
32571
- isError: true
32572
- };
32573
- }
32574
- const text = JSON.stringify(response.data ?? { success: true }, null, 2);
32575
- return {
32576
- content: [{ type: "text", text }]
32577
- };
32578
- } catch (err) {
32579
- const message = err instanceof Error ? err.message : String(err);
32580
- const latency_ms = Date.now() - start;
32581
- const errorEntry = {
32582
- event: "tool_call",
32583
- tool: tool.name,
32584
- status: err instanceof GuardrailError ? "guardrail_block" : "exception",
32585
- latency_ms,
32586
- error: message,
32587
- audit: isDestructive ? true : void 0,
32588
- inputs: isDestructive ? redactSecrets(input) : void 0
32589
- };
32590
- logEvent(errorEntry);
32591
- if (isDestructive) {
32592
- pushAuditEntry({ ts: (/* @__PURE__ */ new Date()).toISOString(), ...errorEntry });
32593
- }
32594
- return {
32595
- content: [{ type: "text", text: `Error: ${message}` }],
32596
- isError: true
32597
- };
32598
- }
32599
- }
32600
- );
32654
+ server.tool(tool.name, tool.description, tool.inputSchema.shape, tool.annotations, createToolHandler(tool));
32601
32655
  }
32602
32656
  server.resource(
32603
32657
  "Recent destructive-call audit log",
@@ -32606,19 +32660,7 @@ server.resource(
32606
32660
  description: "The most recent destructive tool calls and their outcomes (rate limit, refund cap, etc.). Bounded ring buffer; resets on server restart.",
32607
32661
  mimeType: "application/x-ndjson"
32608
32662
  },
32609
- async (uri) => {
32610
- const entries = readAuditEntries();
32611
- const text = entries.map((e) => JSON.stringify(e)).join("\n");
32612
- return {
32613
- contents: [
32614
- {
32615
- uri: uri.href,
32616
- mimeType: "application/x-ndjson",
32617
- text
32618
- }
32619
- ]
32620
- };
32621
- }
32663
+ async (uri) => readAuditLogResource(uri)
32622
32664
  );
32623
32665
  var transport = new StdioServerTransport();
32624
32666
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yawlabs/lemonsqueezy-mcp",
3
- "version": "0.9.0",
3
+ "version": "0.9.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",
@@ -35,6 +35,8 @@
35
35
  "test:integration": "npm run build && node --test dist/integration/*.test.js",
36
36
  "lint": "biome check src/",
37
37
  "lint:fix": "biome check --write src/",
38
+ "gen:containerfile": "node scripts/sync-containerfile.mjs",
39
+ "check:containerfile": "node scripts/sync-containerfile.mjs --check",
38
40
  "prepublishOnly": "npm run build"
39
41
  },
40
42
  "dependencies": {},