@yawlabs/lemonsqueezy-mcp 0.10.12 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +86 -0
- package/README.md +32 -5
- package/dist/index.js +208 -152
- package/package.json +9 -7
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,88 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@yawlabs/lemonsqueezy-mcp` are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and versioning follows [SEMVER.md](./SEMVER.md).
|
|
4
4
|
|
|
5
|
+
## [0.11.0] -- 2026-08-07
|
|
6
|
+
|
|
7
|
+
### Security
|
|
8
|
+
|
|
9
|
+
- **`redactSecrets` no longer degrades to exponential traversal on a shared-reference payload.** The cycle guard tracks the ancestor path (entries removed on the way back up) so a merely *shared* object is redacted normally instead of being falsely reported as `[CIRCULAR]` -- but an ancestor set alone is O(paths), not O(nodes). A "diamond chain" holding two references to the same child at every level has 2^depth paths over depth+1 objects: measured at 3.2 seconds for 23 objects, doubling per level, which at the 32-level cap would block the stdio server for the better part of an hour from inside the audit path. A memo of completed subtrees restores linear behaviour. Reuse is gated on the cached entry having been computed with at least as much depth budget as the current position, so a subtree truncated near the cap is never replayed into a shallower slot where real (possibly secret-bearing) input belongs.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **`validation_error` log/audit status.** Failure entries previously collapsed client mistakes into `exception`, the same bucket as upstream 5xx and genuine faults. The new `ToolInputError` is tagged `validation_error`, so a log scan at `LEMONSQUEEZY_LOG=error` can separate "an agent sent a malformed request" from "the server or upstream faulted". `guardrail_block` (operator policy refused it) is unchanged.
|
|
14
|
+
- **`preflight` hook on the tool contract.** An input-dependent guardrail that runs ahead of the rate limiters. Both refund tools now declare the cap check there, so a rejected over-cap refund no longer consumes the caller's destructive and `money`-class budgets -- previously a client looping on an over-cap amount could exhaust a `money:2/h` allowance on calls that never left the process.
|
|
15
|
+
- **Cross-store disclosure on `ls_list_stores`.** It has no `storeId` field and no parent ID to scope by, so `LEMONSQUEEZY_ALLOWED_STORE_IDS` does not gate it and it enumerates every store the API key can see. This was documented in the README but absent from the tool description an agent actually reads. `ls_list_affiliates` carried the note already.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- **Empty-PATCH guards on the remaining three update tools.** `ls_update_customer`, `ls_update_subscription`, and `ls_update_license_key` sent `attributes: {}` upstream when called with only an ID; `ls_update_webhook` had rejected this locally since 0.10.10. All four now reject before the round-trip and throw `ToolInputError`.
|
|
20
|
+
- **`ls_update_subscription` and `ls_update_customer` descriptions now disclose their conditional-destructive behaviour.** A tool whose destructiveness depends on the input carries `destructiveHint: false`, because the MCP annotation is static and the verdict is per-call -- so an MCP client will not prompt, and the description is the only signal left. `ls_update_subscription` said nothing about pausing or plan switches; `ls_update_customer` mentioned auditing only to point at `ls_archive_customer`, never saying that setting `status: 'archived'` *here* is itself rate-limited and audited. A new invariant in `tools.test.ts` fails the build if any predicate-carrying tool omits the warning.
|
|
21
|
+
- **`Retry-After: " "` no longer means "retry immediately".** `Number("")` is `0`, not `NaN`, so a whitespace-only header fell through the numeric branch and produced a 0 ms wait instead of the 1 s default.
|
|
22
|
+
- **`include: ""` no longer emits a bare `?include=`.** The include schemas set `.max()` but no `.min()`, so an empty string is valid input and split to `[""]`.
|
|
23
|
+
- **Sink response size limit measures UTF-8 bytes, not UTF-16 code units.** `String.length` undercounts every multi-byte character, so a non-ASCII body could exceed the 10 MB limit while reporting a char count under it -- and the message still said "bytes".
|
|
24
|
+
- **A bare `{"error": "..."}` envelope is now read on the management API**, not just the License API, via the shared error handler.
|
|
25
|
+
- **A throwing `isDestructive` predicate can no longer escape as an unhandled rejection.** It is evaluated outside the main try block so it stays in scope for the audit path on the error branch; it now has its own guard that fails closed (treats the call as destructive) and logs the fault.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- **`apiRequest` and `licenseRequest` share one error path and one 2xx body reader.** Roughly 90 lines of near-identical parse/log/decorate logic collapsed to two helpers. `licenseRequest` still deliberately does not bust the API-key cache on 401, since it authenticates with the caller's license key rather than `LEMONSQUEEZY_API_KEY`.
|
|
30
|
+
- **Both generate-invoice tools share one query builder** (`buildInvoiceQuery`) instead of duplicating an eight-field `URLSearchParams` block.
|
|
31
|
+
- **Cross-store notes are generated from each module's `requiredFilters` array**, so the disclosure and the runtime gate cannot drift apart.
|
|
32
|
+
- **`src/index.ts` is free of top-level await.** The version fallback used `await import("node:module")`, which the CJS single-binary build cannot emit; it survived only because both builds define `__VERSION__` and esbuild constant-folded the branch away. A static `createRequire` import removes the dependency on that folding -- verified by bundling to CJS with the define absent.
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- **Opt-in [oam.js](https://oamjs.org) build tooling.** `npm run check:oam` type-checks via `oam check` (tsgo, TypeScript 7 native) -- 2878ms against 4406ms for `tsc --noEmit`, same clean result. `npm run build:binary:oam` builds the standalone binary via `oam compile` instead of the Node SEA path, writing to the same `bin/<platform>-<arch>/` location so the release staging script consumes either unchanged. Neither touches the published npm package.
|
|
37
|
+
|
|
38
|
+
**Node remains the default runtime.** oam runs the server unmodified (verified against 0.8.2: full handshake, all 64 tools, the audit-log resource, working `fetch`, guardrail errors identical to Node), but cold start measured 196ms for Node against 424ms for `oam run` -- and since an MCP client pays startup once per session, that is the cost that matters. oam is also not distributed on npm, so defaulting to it would break `npx` for anyone without it. `src/` stays runtime-agnostic (no `oam:` imports, tests on `node:test`) so the Node path remains a real fallback rather than a nominal one.
|
|
39
|
+
|
|
40
|
+
- `oam/` added to `.gitignore` -- any `oam` invocation writes a bytecode cache there.
|
|
41
|
+
|
|
42
|
+
### Documentation
|
|
43
|
+
|
|
44
|
+
- `0` documented as a valid value for the refund cap and both rate limits (blocks everything; unset/empty means no limit).
|
|
45
|
+
- `LEMONSQUEEZY_LOG` row now lists what each failure `status` means.
|
|
46
|
+
- README's allowlist row no longer describes `ls_list_stores` as routing "by its own resource ID"; the two genuinely ungated list tools are called out separately from the ID-targeted ones.
|
|
47
|
+
|
|
48
|
+
## [0.10.13] -- 2026-06-11
|
|
49
|
+
|
|
50
|
+
### Added
|
|
51
|
+
|
|
52
|
+
- **Cross-platform single-binary pipeline** (Scoop + Homebrew) via `scripts/build-binary.mjs` and `scripts/stage-release-asset.mjs`.
|
|
53
|
+
|
|
54
|
+
### Fixed
|
|
55
|
+
|
|
56
|
+
- **`npm test` runs every test file under POSIX sh**, not only nested ones -- an unquoted `**/*` glob skipped the top-level files.
|
|
57
|
+
|
|
58
|
+
### Changed
|
|
59
|
+
|
|
60
|
+
- Node engine requirement raised to `>=22`; dev-dependency vulnerabilities cleared.
|
|
61
|
+
|
|
62
|
+
## [0.10.12] -- 2026-06-02
|
|
63
|
+
|
|
64
|
+
### Fixed
|
|
65
|
+
|
|
66
|
+
- **Release drift guard compares tag-object SHAs**, so resuming a partial release no longer false-aborts.
|
|
67
|
+
- **`SKIP_LINT=1` escape hatch** for environments where the lint runner itself is broken.
|
|
68
|
+
- **`release.sh` refuses to push if origin's tag drifted from local.**
|
|
69
|
+
- README "Add to Yaw MCP" badge points at the https forwarder.
|
|
70
|
+
|
|
71
|
+
### Added
|
|
72
|
+
|
|
73
|
+
- End-to-end coverage for the refund-cap guard on both refund tools.
|
|
74
|
+
|
|
75
|
+
## [0.10.11] -- 2026-05-28
|
|
76
|
+
|
|
77
|
+
### Changed
|
|
78
|
+
|
|
79
|
+
- **MCP Registry publish folded into `release.sh`**; `release.yml` and the remaining non-release workflows removed. `release.sh` is now the only release path.
|
|
80
|
+
|
|
81
|
+
### Fixed
|
|
82
|
+
|
|
83
|
+
- Confirmation prompt is tty-gated, so non-interactive runs no longer block.
|
|
84
|
+
- `server.json` syncs unconditionally rather than only inside the version-bump branch.
|
|
85
|
+
- Falls back to the `gh` auth token for MCP Registry authentication.
|
|
86
|
+
|
|
5
87
|
## [0.10.10] -- 2026-05-22
|
|
6
88
|
|
|
7
89
|
### Fixed
|
|
@@ -390,6 +472,10 @@ Hardening pass for unattended automation against live billing flows.
|
|
|
390
472
|
Initial release. 59 tools covering all 17 LemonSqueezy API resources.
|
|
391
473
|
|
|
392
474
|
[Unreleased]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.9...HEAD
|
|
475
|
+
[0.11.0]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.13...v0.11.0
|
|
476
|
+
[0.10.13]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.12...v0.10.13
|
|
477
|
+
[0.10.12]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.11...v0.10.12
|
|
478
|
+
[0.10.11]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.10...v0.10.11
|
|
393
479
|
[0.10.10]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.9...v0.10.10
|
|
394
480
|
[0.10.9]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.8...v0.10.9
|
|
395
481
|
[0.10.8]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.7...v0.10.8
|
package/README.md
CHANGED
|
@@ -229,12 +229,12 @@ All configuration is via environment variables. Only `LEMONSQUEEZY_API_KEY` (or
|
|
|
229
229
|
| `LEMONSQUEEZY_API_KEY` | LemonSqueezy API token. |
|
|
230
230
|
| `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. |
|
|
231
231
|
| `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. |
|
|
232
|
-
| `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 (
|
|
233
|
-
| `LEMONSQUEEZY_MAX_REFUND_AMOUNT_CENTS` | Rejects `ls_refund_order` and `ls_refund_subscription_invoice` calls above this amount. |
|
|
234
|
-
| `LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT` |
|
|
232
|
+
| `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 are **not** gated by this allowlist, in two distinct shapes: (a) ID-targeted tools (`ls_refund_order`, `ls_cancel_subscription`, `ls_archive_customer`, `ls_delete_webhook`, `ls_delete_discount`, `ls_update_license_key`) route by their own resource ID, so the caller must already know the ID; (b) `ls_list_stores` and `ls_list_affiliates` take no scoping ID at all and **return rows from every store the API key can see** — `ls_list_stores` will enumerate stores outside the allowlist. Both say so in their own tool descriptions. Other list-by-parent tools (`ls_list_prices`, `ls_list_files`, `ls_list_variants`, `ls_list_order_items`, `ls_list_discount_redemptions`, `ls_list_license_key_instances`, `ls_list_subscription_items`, `ls_list_usage_records`) require a parent-ID filter when the allowlist is set — a partial mitigation, since that parent can itself belong to a non-allowed store. 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. |
|
|
233
|
+
| `LEMONSQUEEZY_MAX_REFUND_AMOUNT_CENTS` | Non-negative integer. Rejects `ls_refund_order` and `ls_refund_subscription_invoice` calls above this amount. Unset or empty means no cap; **`0` is a valid value and blocks every refund** (the schemas require `amount >= 1`), so use it as a kill switch. The check runs *before* the rate limiters, so a rejected over-cap refund does not consume your destructive or `money`-class budget. |
|
|
234
|
+
| `LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT` | Non-negative integer. Max destructive tool calls per 60-second rolling window. Unset or empty means no limit; **`0` blocks every destructive call.** 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"`. |
|
|
235
235
|
| `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. |
|
|
236
|
-
| `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. |
|
|
237
|
-
| `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. |
|
|
236
|
+
| `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. A limit of `0` blocks every call in that class; classes you don't list are unlimited. Malformed entries throw at server startup. In-process per server instance. |
|
|
237
|
+
| `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. Failure entries carry a `status` that identifies the cause: `guardrail_block` (operator policy refused the call), `validation_error` (the client sent a malformed request, e.g. an update with no fields to change), `exception` (something faulted), plus `timeout` / `network_error` and raw HTTP status codes. |
|
|
238
238
|
| `LEMONSQUEEZY_SINK_URL` | Base URL of an optional [@yawlabs/lemonsqueezy-webhook-sink](https://github.com/YawLabs/lemonsqueezy-webhook-sink) instance (e.g. `https://webhooks.example.com`). Trailing slashes are stripped. Enables the `ls_sink_*` reconciliation tools below. Unset: the tools are still registered but return a "not configured" error when called. |
|
|
239
239
|
| `LEMONSQUEEZY_SINK_ADMIN_TOKEN` | Bearer token for the sink's admin endpoints. Must match the sink's `WEBHOOK_SINK_ADMIN_TOKEN`. Required when `LEMONSQUEEZY_SINK_URL` is set; if the sink itself was started without an admin token, its admin endpoints return 404 and `ls_sink_*` calls surface that diagnostically. |
|
|
240
240
|
|
|
@@ -316,6 +316,33 @@ npm run gen:containerfile # regenerate Containerfile
|
|
|
316
316
|
npm run check:containerfile # CI runs this; non-zero exit means the two have drifted
|
|
317
317
|
```
|
|
318
318
|
|
|
319
|
+
## Running on oam.js (optional)
|
|
320
|
+
|
|
321
|
+
[oam.js](https://oamjs.org) runs this server unmodified. Verified against oam 0.8.2: full MCP handshake, all 64 tools, the `lemonsqueezy://audit-log` resource, working `fetch`, and guardrail rejections with error text identical to Node.
|
|
322
|
+
|
|
323
|
+
```jsonc
|
|
324
|
+
{
|
|
325
|
+
"mcpServers": {
|
|
326
|
+
"lemonsqueezy": {
|
|
327
|
+
"command": "oam",
|
|
328
|
+
"args": ["run", "/path/to/lemonsqueezy-mcp/dist/index.js"],
|
|
329
|
+
"env": { "LEMONSQUEEZY_API_KEY": "..." }
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
Note the `--` separator if you pass arguments to the server rather than to oam: `oam run dist/index.js -- version`.
|
|
336
|
+
|
|
337
|
+
**Node stays the default, deliberately.** An MCP client cold-starts this server once per session, so startup is the cost that actually gets paid — and on the machine this was measured on, Node won: **196ms median against 424ms for `oam run`** (8 runs each, `version` subcommand, which exercises full boot plus tool registration). Making oam the default would mean either a launcher that probes for it on every start — a cost paid by everyone, including the majority who do not have oam installed — or making oam a hard requirement, which would break `npx @yawlabs/lemonsqueezy-mcp` for every user without it, since oam is not distributed on npm. Neither is worth it to reach a runtime that is slower here. Measure on your own hardware before concluding anything; if oam wins on yours, the config above is all you need.
|
|
338
|
+
|
|
339
|
+
Two places oam *does* win for this repo, both opt-in and neither touching the npm package:
|
|
340
|
+
|
|
341
|
+
- **`npm run check:oam`** — type-checks via `oam check` (tsgo, TypeScript 7 native). Measured 2878ms against 4406ms for `tsc --noEmit`, same clean result. `npx tsc --noEmit` remains the portable default and is what the pre-commit checklist calls for.
|
|
342
|
+
- **`npm run build:binary:oam`** — builds the standalone binary via `oam compile` instead of the Node SEA path. Measured 57.14 MB. Writes to the same `bin/<platform>-<arch>/` path as `npm run build:binary`, so the release staging script consumes either unchanged — run one or the other, not both. If you redistribute that binary it embeds oam's runtime, so ship oam's `LICENSE`, `NOTICE` and `THIRD_PARTY_LICENSES.md` with it.
|
|
343
|
+
|
|
344
|
+
The source stays runtime-agnostic on purpose: no `oam:` imports anywhere, and tests stay on `node:test`. That is what keeps the Node fallback real rather than nominal — an `oam:test` or `oam:`-prefixed import would make "falls back to Node" false the moment it landed. Any `oam` invocation writes a bytecode cache to `oam/` in the working directory; that path is gitignored.
|
|
345
|
+
|
|
319
346
|
## Releasing
|
|
320
347
|
|
|
321
348
|
Two paths from a clean checkout of `main`. Both produce the same artifact (npm publish with provenance + GitHub release).
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,11 @@ var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
|
6
6
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
8
|
var __commonJS = (cb, mod) => function __require() {
|
|
9
|
-
|
|
9
|
+
try {
|
|
10
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
11
|
+
} catch (e) {
|
|
12
|
+
throw mod = 0, e;
|
|
13
|
+
}
|
|
10
14
|
};
|
|
11
15
|
var __export = (target, all) => {
|
|
12
16
|
for (var name in all)
|
|
@@ -6885,6 +6889,9 @@ var require_dist = __commonJS({
|
|
|
6885
6889
|
}
|
|
6886
6890
|
});
|
|
6887
6891
|
|
|
6892
|
+
// src/index.ts
|
|
6893
|
+
import "node:module";
|
|
6894
|
+
|
|
6888
6895
|
// node_modules/zod/v3/external.js
|
|
6889
6896
|
var external_exports = {};
|
|
6890
6897
|
__export(external_exports, {
|
|
@@ -21104,6 +21111,12 @@ var GuardrailError = class extends Error {
|
|
|
21104
21111
|
this.name = "GuardrailError";
|
|
21105
21112
|
}
|
|
21106
21113
|
};
|
|
21114
|
+
var ToolInputError = class extends Error {
|
|
21115
|
+
constructor(message) {
|
|
21116
|
+
super(message);
|
|
21117
|
+
this.name = "ToolInputError";
|
|
21118
|
+
}
|
|
21119
|
+
};
|
|
21107
21120
|
var AUTHORITY_CLASSES = ["read", "pii", "mutate", "money", "recurring", "key", "webhook"];
|
|
21108
21121
|
function isAuthorityClass(s) {
|
|
21109
21122
|
return AUTHORITY_CLASSES.includes(s);
|
|
@@ -21288,7 +21301,14 @@ function getLogLevel() {
|
|
|
21288
21301
|
if (v === "error") return "error";
|
|
21289
21302
|
return "off";
|
|
21290
21303
|
}
|
|
21291
|
-
var ERROR_STATUS_TAGS = /* @__PURE__ */ new Set([
|
|
21304
|
+
var ERROR_STATUS_TAGS = /* @__PURE__ */ new Set([
|
|
21305
|
+
"error",
|
|
21306
|
+
"exception",
|
|
21307
|
+
"guardrail_block",
|
|
21308
|
+
"validation_error",
|
|
21309
|
+
"timeout",
|
|
21310
|
+
"network_error"
|
|
21311
|
+
]);
|
|
21292
21312
|
function isErrorEntry(entry) {
|
|
21293
21313
|
if (entry.error !== void 0) return true;
|
|
21294
21314
|
if (typeof entry.status === "string") return ERROR_STATUS_TAGS.has(entry.status);
|
|
@@ -21370,6 +21390,7 @@ var JITTER_FRACTION = 0.25;
|
|
|
21370
21390
|
function parseRetryAfterMs(header) {
|
|
21371
21391
|
if (!header) return DEFAULT_RETRY_WAIT_MS;
|
|
21372
21392
|
const trimmed = header.trim();
|
|
21393
|
+
if (trimmed === "") return DEFAULT_RETRY_WAIT_MS;
|
|
21373
21394
|
const seconds = Number(trimmed);
|
|
21374
21395
|
if (!Number.isNaN(seconds)) {
|
|
21375
21396
|
return seconds >= 0 ? seconds * 1e3 : DEFAULT_RETRY_WAIT_MS;
|
|
@@ -21582,7 +21603,10 @@ function buildQuery(params) {
|
|
|
21582
21603
|
if (!params) return "";
|
|
21583
21604
|
const parts = [];
|
|
21584
21605
|
if (params.include?.length) {
|
|
21585
|
-
|
|
21606
|
+
const segments = params.include.map((s) => s.trim()).filter(Boolean);
|
|
21607
|
+
if (segments.length > 0) {
|
|
21608
|
+
parts.push(`include=${encodeURIComponent(segments.join(","))}`);
|
|
21609
|
+
}
|
|
21586
21610
|
}
|
|
21587
21611
|
if (params.filter) {
|
|
21588
21612
|
for (const [key, value] of Object.entries(params.filter)) {
|
|
@@ -21602,13 +21626,57 @@ function buildQuery(params) {
|
|
|
21602
21626
|
function decorateError(error2, requestId) {
|
|
21603
21627
|
return requestId ? `${error2} (request_id: ${requestId})` : error2;
|
|
21604
21628
|
}
|
|
21629
|
+
async function handleErrorResponse(res, route, latency_ms, requestId) {
|
|
21630
|
+
const errorBody = await res.text();
|
|
21631
|
+
try {
|
|
21632
|
+
const parsed = JSON.parse(errorBody);
|
|
21633
|
+
const detail = parsed.errors?.[0]?.detail ?? parsed.error ?? errorBody;
|
|
21634
|
+
logEvent({
|
|
21635
|
+
event: "http_call",
|
|
21636
|
+
method: route.method,
|
|
21637
|
+
path: route.path,
|
|
21638
|
+
status: res.status,
|
|
21639
|
+
latency_ms,
|
|
21640
|
+
request_id: requestId,
|
|
21641
|
+
error: detail
|
|
21642
|
+
});
|
|
21643
|
+
return {
|
|
21644
|
+
ok: false,
|
|
21645
|
+
status: res.status,
|
|
21646
|
+
data: parsed,
|
|
21647
|
+
error: decorateError(detail, requestId),
|
|
21648
|
+
requestId
|
|
21649
|
+
};
|
|
21650
|
+
} catch {
|
|
21651
|
+
logEvent({
|
|
21652
|
+
event: "http_call",
|
|
21653
|
+
method: route.method,
|
|
21654
|
+
path: route.path,
|
|
21655
|
+
status: res.status,
|
|
21656
|
+
latency_ms,
|
|
21657
|
+
request_id: requestId,
|
|
21658
|
+
error: errorBody
|
|
21659
|
+
});
|
|
21660
|
+
return {
|
|
21661
|
+
ok: false,
|
|
21662
|
+
status: res.status,
|
|
21663
|
+
error: decorateError(errorBody, requestId),
|
|
21664
|
+
requestId
|
|
21665
|
+
};
|
|
21666
|
+
}
|
|
21667
|
+
}
|
|
21668
|
+
async function readJsonBody(res) {
|
|
21669
|
+
const bodyText = await res.text();
|
|
21670
|
+
return bodyText.trim() ? JSON.parse(bodyText) : void 0;
|
|
21671
|
+
}
|
|
21605
21672
|
function formatTimeoutMessage(err, fallbackElapsedMs) {
|
|
21673
|
+
const plural = (n) => `${n} attempt${n === 1 ? "" : "s"}`;
|
|
21606
21674
|
if (isRetryTimeoutError(err)) {
|
|
21607
21675
|
const seconds2 = Math.max(1, Math.round(err.elapsedMs / 1e3));
|
|
21608
|
-
return `Request timed out after ${seconds2}s (${err.attempts}
|
|
21676
|
+
return `Request timed out after ${seconds2}s (${plural(err.attempts)})`;
|
|
21609
21677
|
}
|
|
21610
21678
|
const seconds = Math.max(1, Math.round(fallbackElapsedMs / 1e3));
|
|
21611
|
-
return `Request timed out after ${seconds}s (1
|
|
21679
|
+
return `Request timed out after ${seconds}s (${plural(1)})`;
|
|
21612
21680
|
}
|
|
21613
21681
|
async function apiRequest(method, path, body) {
|
|
21614
21682
|
const start = Date.now();
|
|
@@ -21644,43 +21712,7 @@ async function apiRequest(method, path, body) {
|
|
|
21644
21712
|
if (res.status === 401 || res.status === 403) {
|
|
21645
21713
|
invalidateApiKeyCache();
|
|
21646
21714
|
}
|
|
21647
|
-
|
|
21648
|
-
try {
|
|
21649
|
-
const parsed = JSON.parse(errorBody);
|
|
21650
|
-
const detail = parsed.errors?.[0]?.detail ?? errorBody;
|
|
21651
|
-
logEvent({
|
|
21652
|
-
event: "http_call",
|
|
21653
|
-
method,
|
|
21654
|
-
path,
|
|
21655
|
-
status: res.status,
|
|
21656
|
-
latency_ms,
|
|
21657
|
-
request_id: requestId,
|
|
21658
|
-
error: detail
|
|
21659
|
-
});
|
|
21660
|
-
return {
|
|
21661
|
-
ok: false,
|
|
21662
|
-
status: res.status,
|
|
21663
|
-
data: parsed,
|
|
21664
|
-
error: decorateError(detail, requestId),
|
|
21665
|
-
requestId
|
|
21666
|
-
};
|
|
21667
|
-
} catch {
|
|
21668
|
-
logEvent({
|
|
21669
|
-
event: "http_call",
|
|
21670
|
-
method,
|
|
21671
|
-
path,
|
|
21672
|
-
status: res.status,
|
|
21673
|
-
latency_ms,
|
|
21674
|
-
request_id: requestId,
|
|
21675
|
-
error: errorBody
|
|
21676
|
-
});
|
|
21677
|
-
return {
|
|
21678
|
-
ok: false,
|
|
21679
|
-
status: res.status,
|
|
21680
|
-
error: decorateError(errorBody, requestId),
|
|
21681
|
-
requestId
|
|
21682
|
-
};
|
|
21683
|
-
}
|
|
21715
|
+
return handleErrorResponse(res, { method, path }, latency_ms, requestId);
|
|
21684
21716
|
}
|
|
21685
21717
|
logEvent({
|
|
21686
21718
|
event: "http_call",
|
|
@@ -21693,9 +21725,7 @@ async function apiRequest(method, path, body) {
|
|
|
21693
21725
|
if (res.status === 204) {
|
|
21694
21726
|
return { ok: true, status: res.status, requestId };
|
|
21695
21727
|
}
|
|
21696
|
-
|
|
21697
|
-
const data = bodyText.trim() ? JSON.parse(bodyText) : void 0;
|
|
21698
|
-
return { ok: true, status: res.status, data, requestId };
|
|
21728
|
+
return { ok: true, status: res.status, data: await readJsonBody(res), requestId };
|
|
21699
21729
|
}
|
|
21700
21730
|
async function licenseRequest(path, body) {
|
|
21701
21731
|
const url = `${BASE_URL}${path}`;
|
|
@@ -21735,44 +21765,7 @@ async function licenseRequest(path, body) {
|
|
|
21735
21765
|
const latency_ms = Date.now() - start;
|
|
21736
21766
|
const requestId = res.headers.get("x-request-id") ?? void 0;
|
|
21737
21767
|
if (!res.ok) {
|
|
21738
|
-
|
|
21739
|
-
try {
|
|
21740
|
-
const parsed = JSON.parse(errorBody);
|
|
21741
|
-
const p = parsed;
|
|
21742
|
-
const detail = p.errors?.[0]?.detail ?? p.error ?? errorBody;
|
|
21743
|
-
logEvent({
|
|
21744
|
-
event: "http_call",
|
|
21745
|
-
method: "POST",
|
|
21746
|
-
path,
|
|
21747
|
-
status: res.status,
|
|
21748
|
-
latency_ms,
|
|
21749
|
-
request_id: requestId,
|
|
21750
|
-
error: detail
|
|
21751
|
-
});
|
|
21752
|
-
return {
|
|
21753
|
-
ok: false,
|
|
21754
|
-
status: res.status,
|
|
21755
|
-
data: parsed,
|
|
21756
|
-
error: decorateError(detail, requestId),
|
|
21757
|
-
requestId
|
|
21758
|
-
};
|
|
21759
|
-
} catch {
|
|
21760
|
-
logEvent({
|
|
21761
|
-
event: "http_call",
|
|
21762
|
-
method: "POST",
|
|
21763
|
-
path,
|
|
21764
|
-
status: res.status,
|
|
21765
|
-
latency_ms,
|
|
21766
|
-
request_id: requestId,
|
|
21767
|
-
error: errorBody
|
|
21768
|
-
});
|
|
21769
|
-
return {
|
|
21770
|
-
ok: false,
|
|
21771
|
-
status: res.status,
|
|
21772
|
-
error: decorateError(errorBody, requestId),
|
|
21773
|
-
requestId
|
|
21774
|
-
};
|
|
21775
|
-
}
|
|
21768
|
+
return handleErrorResponse(res, { method: "POST", path }, latency_ms, requestId);
|
|
21776
21769
|
}
|
|
21777
21770
|
logEvent({
|
|
21778
21771
|
event: "http_call",
|
|
@@ -21782,9 +21775,7 @@ async function licenseRequest(path, body) {
|
|
|
21782
21775
|
latency_ms,
|
|
21783
21776
|
request_id: requestId
|
|
21784
21777
|
});
|
|
21785
|
-
|
|
21786
|
-
const data = bodyText.trim() ? JSON.parse(bodyText) : void 0;
|
|
21787
|
-
return { ok: true, status: res.status, data, requestId };
|
|
21778
|
+
return { ok: true, status: res.status, data: await readJsonBody(res), requestId };
|
|
21788
21779
|
}
|
|
21789
21780
|
function getHandler(endpoint, idField) {
|
|
21790
21781
|
return async (input) => {
|
|
@@ -21808,6 +21799,32 @@ function listHandler(endpoint, filterMap = {}) {
|
|
|
21808
21799
|
};
|
|
21809
21800
|
return Object.assign(handler, { filterMap });
|
|
21810
21801
|
}
|
|
21802
|
+
var INVOICE_FIELD_MAP = {
|
|
21803
|
+
name: "name",
|
|
21804
|
+
address: "address",
|
|
21805
|
+
city: "city",
|
|
21806
|
+
state: "state",
|
|
21807
|
+
zipCode: "zip_code",
|
|
21808
|
+
country: "country",
|
|
21809
|
+
notes: "notes",
|
|
21810
|
+
locale: "locale"
|
|
21811
|
+
};
|
|
21812
|
+
function buildInvoiceQuery(input) {
|
|
21813
|
+
const params = new URLSearchParams();
|
|
21814
|
+
for (const [inputKey, apiKey] of Object.entries(INVOICE_FIELD_MAP)) {
|
|
21815
|
+
const value = input[inputKey];
|
|
21816
|
+
if (value !== void 0) params.set(apiKey, value);
|
|
21817
|
+
}
|
|
21818
|
+
const qs = params.toString();
|
|
21819
|
+
return qs ? `?${qs}` : "";
|
|
21820
|
+
}
|
|
21821
|
+
var CROSS_STORE_TRAILER = "Pair with a scoped LemonSqueezy API key for true cross-store enforcement -- the API key's visibility is the true boundary.";
|
|
21822
|
+
function crossStoreFilterNote(filters) {
|
|
21823
|
+
return `Cross-store note: when LEMONSQUEEZY_ALLOWED_STORE_IDS is set, this tool requires at least one of: ${filters.join(", ")}. Even with that set, ${CROSS_STORE_TRAILER}`;
|
|
21824
|
+
}
|
|
21825
|
+
function crossStoreUngatedNote(returns) {
|
|
21826
|
+
return `Cross-store note: LEMONSQUEEZY_ALLOWED_STORE_IDS does NOT gate this tool -- it has no storeId field and no parent ID filter to scope by, so it returns ${returns}. ${CROSS_STORE_TRAILER}`;
|
|
21827
|
+
}
|
|
21811
21828
|
async function apiGet(path) {
|
|
21812
21829
|
return apiRequest("GET", path);
|
|
21813
21830
|
}
|
|
@@ -21843,7 +21860,9 @@ var affiliateTools = [
|
|
|
21843
21860
|
{
|
|
21844
21861
|
name: "ls_list_affiliates",
|
|
21845
21862
|
authorityClass: "read",
|
|
21846
|
-
description:
|
|
21863
|
+
description: `List all affiliates for the authenticated user's stores, optionally filtered by user email. Results are paginated \u2014 check meta.page in the response for currentPage, lastPage, and total. ${crossStoreUngatedNote(
|
|
21864
|
+
"affiliates tied to every store the API key can see, including non-allowed ones"
|
|
21865
|
+
)}`,
|
|
21847
21866
|
annotations: {
|
|
21848
21867
|
title: "List affiliates",
|
|
21849
21868
|
readOnlyHint: true,
|
|
@@ -22039,7 +22058,7 @@ var customerTools = [
|
|
|
22039
22058
|
{
|
|
22040
22059
|
name: "ls_update_customer",
|
|
22041
22060
|
authorityClass: "pii",
|
|
22042
|
-
description: "Update an existing customer's name, email, city, region, country, or status. The only supported status value is 'archived'
|
|
22061
|
+
description: "Update an existing customer's name, email, city, region, country, or status. The only supported status value is 'archived'; setting it here is the same operation as ls_archive_customer and is treated as destructive (rate-limited and audited). Other field edits are not.",
|
|
22043
22062
|
annotations: {
|
|
22044
22063
|
title: "Update customer",
|
|
22045
22064
|
readOnlyHint: false,
|
|
@@ -22073,6 +22092,11 @@ var customerTools = [
|
|
|
22073
22092
|
if (input.region !== void 0) attributes.region = input.region;
|
|
22074
22093
|
if (input.country !== void 0) attributes.country = input.country;
|
|
22075
22094
|
if (input.status !== void 0) attributes.status = input.status;
|
|
22095
|
+
if (Object.keys(attributes).length === 0) {
|
|
22096
|
+
throw new ToolInputError(
|
|
22097
|
+
"ls_update_customer requires at least one of: name, email, city, region, country, status"
|
|
22098
|
+
);
|
|
22099
|
+
}
|
|
22076
22100
|
return apiPatch(`/customers/${encodePath(input.customerId)}`, {
|
|
22077
22101
|
data: {
|
|
22078
22102
|
type: "customers",
|
|
@@ -22109,6 +22133,7 @@ var customerTools = [
|
|
|
22109
22133
|
];
|
|
22110
22134
|
|
|
22111
22135
|
// src/tools/discount-redemptions.ts
|
|
22136
|
+
var LIST_DISCOUNT_REDEMPTIONS_FILTERS = ["discountId", "orderId"];
|
|
22112
22137
|
var discountRedemptionTools = [
|
|
22113
22138
|
{
|
|
22114
22139
|
name: "ls_get_discount_redemption",
|
|
@@ -22130,7 +22155,7 @@ var discountRedemptionTools = [
|
|
|
22130
22155
|
{
|
|
22131
22156
|
name: "ls_list_discount_redemptions",
|
|
22132
22157
|
authorityClass: "read",
|
|
22133
|
-
description:
|
|
22158
|
+
description: `List all discount redemptions, optionally filtered by discount or order. Results are paginated \u2014 check meta.page in the response for currentPage, lastPage, and total. ${crossStoreFilterNote(LIST_DISCOUNT_REDEMPTIONS_FILTERS)}`,
|
|
22134
22159
|
annotations: {
|
|
22135
22160
|
title: "List discount redemptions",
|
|
22136
22161
|
readOnlyHint: true,
|
|
@@ -22145,7 +22170,7 @@ var discountRedemptionTools = [
|
|
|
22145
22170
|
pageNumber: external_exports.number().int().min(1).optional().describe("Page number (1-indexed)"),
|
|
22146
22171
|
pageSize: external_exports.number().int().min(1).max(100).optional().describe("Results per page (1-100)")
|
|
22147
22172
|
}),
|
|
22148
|
-
requiredFilters:
|
|
22173
|
+
requiredFilters: LIST_DISCOUNT_REDEMPTIONS_FILTERS,
|
|
22149
22174
|
handler: listHandler("/discount-redemptions", { discountId: "discount_id", orderId: "order_id" })
|
|
22150
22175
|
}
|
|
22151
22176
|
];
|
|
@@ -22268,6 +22293,7 @@ var discountTools = [
|
|
|
22268
22293
|
];
|
|
22269
22294
|
|
|
22270
22295
|
// src/tools/files.ts
|
|
22296
|
+
var LIST_FILES_FILTERS = ["variantId"];
|
|
22271
22297
|
var fileTools = [
|
|
22272
22298
|
{
|
|
22273
22299
|
name: "ls_get_file",
|
|
@@ -22289,7 +22315,7 @@ var fileTools = [
|
|
|
22289
22315
|
{
|
|
22290
22316
|
name: "ls_list_files",
|
|
22291
22317
|
authorityClass: "read",
|
|
22292
|
-
description:
|
|
22318
|
+
description: `List all files, optionally filtered by variant. Results are paginated \u2014 check meta.page in the response for currentPage, lastPage, and total. ${crossStoreFilterNote(LIST_FILES_FILTERS)}`,
|
|
22293
22319
|
annotations: {
|
|
22294
22320
|
title: "List files",
|
|
22295
22321
|
readOnlyHint: true,
|
|
@@ -22303,12 +22329,13 @@ var fileTools = [
|
|
|
22303
22329
|
pageNumber: external_exports.number().int().min(1).optional().describe("Page number (1-indexed)"),
|
|
22304
22330
|
pageSize: external_exports.number().int().min(1).max(100).optional().describe("Results per page (1-100)")
|
|
22305
22331
|
}),
|
|
22306
|
-
requiredFilters:
|
|
22332
|
+
requiredFilters: LIST_FILES_FILTERS,
|
|
22307
22333
|
handler: listHandler("/files", { variantId: "variant_id" })
|
|
22308
22334
|
}
|
|
22309
22335
|
];
|
|
22310
22336
|
|
|
22311
22337
|
// src/tools/license-key-instances.ts
|
|
22338
|
+
var LIST_LICENSE_KEY_INSTANCES_FILTERS = ["licenseKeyId"];
|
|
22312
22339
|
var licenseKeyInstanceTools = [
|
|
22313
22340
|
{
|
|
22314
22341
|
name: "ls_get_license_key_instance",
|
|
@@ -22330,7 +22357,7 @@ var licenseKeyInstanceTools = [
|
|
|
22330
22357
|
{
|
|
22331
22358
|
name: "ls_list_license_key_instances",
|
|
22332
22359
|
authorityClass: "read",
|
|
22333
|
-
description:
|
|
22360
|
+
description: `List all license key instances (activations), optionally filtered by license key. Results are paginated \u2014 check meta.page in the response for currentPage, lastPage, and total. ${crossStoreFilterNote(LIST_LICENSE_KEY_INSTANCES_FILTERS)}`,
|
|
22334
22361
|
annotations: {
|
|
22335
22362
|
title: "List license key instances",
|
|
22336
22363
|
readOnlyHint: true,
|
|
@@ -22344,7 +22371,7 @@ var licenseKeyInstanceTools = [
|
|
|
22344
22371
|
pageNumber: external_exports.number().int().min(1).optional().describe("Page number (1-indexed)"),
|
|
22345
22372
|
pageSize: external_exports.number().int().min(1).max(100).optional().describe("Results per page (1-100)")
|
|
22346
22373
|
}),
|
|
22347
|
-
requiredFilters:
|
|
22374
|
+
requiredFilters: LIST_LICENSE_KEY_INSTANCES_FILTERS,
|
|
22348
22375
|
handler: listHandler("/license-key-instances", { licenseKeyId: "license_key_id" })
|
|
22349
22376
|
}
|
|
22350
22377
|
];
|
|
@@ -22429,6 +22456,11 @@ var licenseKeyTools = [
|
|
|
22429
22456
|
if (input.activationLimit !== void 0) attributes.activation_limit = input.activationLimit;
|
|
22430
22457
|
if (input.disabled !== void 0) attributes.disabled = input.disabled;
|
|
22431
22458
|
if (input.expiresAt !== void 0) attributes.expires_at = input.expiresAt;
|
|
22459
|
+
if (Object.keys(attributes).length === 0) {
|
|
22460
|
+
throw new ToolInputError(
|
|
22461
|
+
"ls_update_license_key requires at least one of: activationLimit, disabled, expiresAt"
|
|
22462
|
+
);
|
|
22463
|
+
}
|
|
22432
22464
|
return apiPatch(`/license-keys/${encodePath(input.licenseKeyId)}`, {
|
|
22433
22465
|
data: {
|
|
22434
22466
|
type: "license-keys",
|
|
@@ -22510,6 +22542,7 @@ var licenseTools = [
|
|
|
22510
22542
|
];
|
|
22511
22543
|
|
|
22512
22544
|
// src/tools/order-items.ts
|
|
22545
|
+
var LIST_ORDER_ITEMS_FILTERS = ["orderId", "productId", "variantId"];
|
|
22513
22546
|
var orderItemTools = [
|
|
22514
22547
|
{
|
|
22515
22548
|
name: "ls_get_order_item",
|
|
@@ -22531,7 +22564,7 @@ var orderItemTools = [
|
|
|
22531
22564
|
{
|
|
22532
22565
|
name: "ls_list_order_items",
|
|
22533
22566
|
authorityClass: "read",
|
|
22534
|
-
description:
|
|
22567
|
+
description: `List all order items, optionally filtered by order or product. Results are paginated \u2014 check meta.page in the response for currentPage, lastPage, and total. ${crossStoreFilterNote(LIST_ORDER_ITEMS_FILTERS)}`,
|
|
22535
22568
|
annotations: {
|
|
22536
22569
|
title: "List order items",
|
|
22537
22570
|
readOnlyHint: true,
|
|
@@ -22547,7 +22580,7 @@ var orderItemTools = [
|
|
|
22547
22580
|
pageNumber: external_exports.number().int().min(1).optional().describe("Page number (1-indexed)"),
|
|
22548
22581
|
pageSize: external_exports.number().int().min(1).max(100).optional().describe("Results per page (1-100)")
|
|
22549
22582
|
}),
|
|
22550
|
-
requiredFilters:
|
|
22583
|
+
requiredFilters: LIST_ORDER_ITEMS_FILTERS,
|
|
22551
22584
|
handler: listHandler("/order-items", { orderId: "order_id", productId: "product_id", variantId: "variant_id" })
|
|
22552
22585
|
}
|
|
22553
22586
|
];
|
|
@@ -22618,17 +22651,7 @@ var orderTools = [
|
|
|
22618
22651
|
locale: external_exports.string().max(1e4).optional().describe("Invoice language locale (e.g. 'en', 'fr', 'de')")
|
|
22619
22652
|
}),
|
|
22620
22653
|
handler: async (input) => {
|
|
22621
|
-
|
|
22622
|
-
if (input.name !== void 0) params.set("name", input.name);
|
|
22623
|
-
if (input.address !== void 0) params.set("address", input.address);
|
|
22624
|
-
if (input.city !== void 0) params.set("city", input.city);
|
|
22625
|
-
if (input.state !== void 0) params.set("state", input.state);
|
|
22626
|
-
if (input.zipCode !== void 0) params.set("zip_code", input.zipCode);
|
|
22627
|
-
if (input.country !== void 0) params.set("country", input.country);
|
|
22628
|
-
if (input.notes !== void 0) params.set("notes", input.notes);
|
|
22629
|
-
if (input.locale !== void 0) params.set("locale", input.locale);
|
|
22630
|
-
const qs = params.toString();
|
|
22631
|
-
return apiPost(`/orders/${encodePath(input.orderId)}/generate-invoice${qs ? `?${qs}` : ""}`);
|
|
22654
|
+
return apiPost(`/orders/${encodePath(input.orderId)}/generate-invoice${buildInvoiceQuery(input)}`);
|
|
22632
22655
|
}
|
|
22633
22656
|
},
|
|
22634
22657
|
{
|
|
@@ -22646,6 +22669,12 @@ var orderTools = [
|
|
|
22646
22669
|
orderId: lsIdSchema.describe("The order ID to refund"),
|
|
22647
22670
|
amount: external_exports.number().int().min(1).describe("Refund amount in cents (e.g. 1000 = $10.00)")
|
|
22648
22671
|
}),
|
|
22672
|
+
// Run the cap check ahead of the rate limiters (see `preflight` on
|
|
22673
|
+
// RegisterableTool) so a rejected over-cap refund does not consume the
|
|
22674
|
+
// caller's money-class and destructive budgets. The handler repeats the
|
|
22675
|
+
// check for direct callers that bypass the wrapper; checkRefundAmount is
|
|
22676
|
+
// pure, so running it twice is free.
|
|
22677
|
+
preflight: (input) => checkRefundAmount(input.amount),
|
|
22649
22678
|
handler: async (input) => {
|
|
22650
22679
|
checkRefundAmount(input.amount);
|
|
22651
22680
|
return apiPost(`/orders/${encodePath(input.orderId)}/refund`, {
|
|
@@ -22656,6 +22685,7 @@ var orderTools = [
|
|
|
22656
22685
|
];
|
|
22657
22686
|
|
|
22658
22687
|
// src/tools/prices.ts
|
|
22688
|
+
var LIST_PRICES_FILTERS = ["variantId"];
|
|
22659
22689
|
var priceTools = [
|
|
22660
22690
|
{
|
|
22661
22691
|
name: "ls_get_price",
|
|
@@ -22677,7 +22707,7 @@ var priceTools = [
|
|
|
22677
22707
|
{
|
|
22678
22708
|
name: "ls_list_prices",
|
|
22679
22709
|
authorityClass: "read",
|
|
22680
|
-
description:
|
|
22710
|
+
description: `List all prices, optionally filtered by variant. Results are paginated \u2014 check meta.page in the response for currentPage, lastPage, and total. ${crossStoreFilterNote(LIST_PRICES_FILTERS)}`,
|
|
22681
22711
|
annotations: {
|
|
22682
22712
|
title: "List prices",
|
|
22683
22713
|
readOnlyHint: true,
|
|
@@ -22691,7 +22721,7 @@ var priceTools = [
|
|
|
22691
22721
|
pageNumber: external_exports.number().int().min(1).optional().describe("Page number (1-indexed)"),
|
|
22692
22722
|
pageSize: external_exports.number().int().min(1).max(100).optional().describe("Results per page (1-100)")
|
|
22693
22723
|
}),
|
|
22694
|
-
requiredFilters:
|
|
22724
|
+
requiredFilters: LIST_PRICES_FILTERS,
|
|
22695
22725
|
handler: listHandler("/prices", { variantId: "variant_id" })
|
|
22696
22726
|
}
|
|
22697
22727
|
];
|
|
@@ -22847,10 +22877,11 @@ async function sinkRequest(config2, method, pathAndQuery) {
|
|
|
22847
22877
|
}
|
|
22848
22878
|
const text = readResult.text;
|
|
22849
22879
|
if (!text.trim()) return { ok: true, data: {} };
|
|
22850
|
-
|
|
22880
|
+
const byteLength = Buffer.byteLength(text, "utf8");
|
|
22881
|
+
if (byteLength > MAX_BODY_SIZE_BYTES) {
|
|
22851
22882
|
return {
|
|
22852
22883
|
ok: false,
|
|
22853
|
-
error: `Sink response body too large: ${
|
|
22884
|
+
error: `Sink response body too large: ${byteLength} bytes exceeds ${MAX_BODY_SIZE_BYTES} byte limit`
|
|
22854
22885
|
};
|
|
22855
22886
|
}
|
|
22856
22887
|
try {
|
|
@@ -22954,7 +22985,9 @@ var storeTools = [
|
|
|
22954
22985
|
{
|
|
22955
22986
|
name: "ls_list_stores",
|
|
22956
22987
|
authorityClass: "read",
|
|
22957
|
-
description:
|
|
22988
|
+
description: `List all stores for the authenticated user. Results are paginated \u2014 check meta.page in the response for currentPage, lastPage, and total. ${crossStoreUngatedNote(
|
|
22989
|
+
"every store the API key can see, including stores outside the allowlist"
|
|
22990
|
+
)}`,
|
|
22958
22991
|
annotations: {
|
|
22959
22992
|
title: "List stores",
|
|
22960
22993
|
readOnlyHint: true,
|
|
@@ -23042,18 +23075,8 @@ var subscriptionInvoiceTools = [
|
|
|
23042
23075
|
locale: external_exports.string().max(1e4).optional().describe("Invoice language locale (e.g. 'en', 'fr', 'de')")
|
|
23043
23076
|
}),
|
|
23044
23077
|
handler: async (input) => {
|
|
23045
|
-
const params = new URLSearchParams();
|
|
23046
|
-
if (input.name !== void 0) params.set("name", input.name);
|
|
23047
|
-
if (input.address !== void 0) params.set("address", input.address);
|
|
23048
|
-
if (input.city !== void 0) params.set("city", input.city);
|
|
23049
|
-
if (input.state !== void 0) params.set("state", input.state);
|
|
23050
|
-
if (input.zipCode !== void 0) params.set("zip_code", input.zipCode);
|
|
23051
|
-
if (input.country !== void 0) params.set("country", input.country);
|
|
23052
|
-
if (input.notes !== void 0) params.set("notes", input.notes);
|
|
23053
|
-
if (input.locale !== void 0) params.set("locale", input.locale);
|
|
23054
|
-
const qs = params.toString();
|
|
23055
23078
|
return apiPost(
|
|
23056
|
-
`/subscription-invoices/${encodePath(input.subscriptionInvoiceId)}/generate-invoice${
|
|
23079
|
+
`/subscription-invoices/${encodePath(input.subscriptionInvoiceId)}/generate-invoice${buildInvoiceQuery(input)}`
|
|
23057
23080
|
);
|
|
23058
23081
|
}
|
|
23059
23082
|
},
|
|
@@ -23072,6 +23095,9 @@ var subscriptionInvoiceTools = [
|
|
|
23072
23095
|
subscriptionInvoiceId: lsIdSchema.describe("The subscription invoice ID to refund"),
|
|
23073
23096
|
amount: external_exports.number().int().min(1).describe("Refund amount in cents (e.g. 1000 = $10.00)")
|
|
23074
23097
|
}),
|
|
23098
|
+
// See the matching comment on ls_refund_order: the cap check runs ahead of
|
|
23099
|
+
// the rate limiters so a rejection costs the caller no budget.
|
|
23100
|
+
preflight: (input) => checkRefundAmount(input.amount),
|
|
23075
23101
|
handler: async (input) => {
|
|
23076
23102
|
checkRefundAmount(input.amount);
|
|
23077
23103
|
return apiPost(`/subscription-invoices/${encodePath(input.subscriptionInvoiceId)}/refund`, {
|
|
@@ -23086,6 +23112,7 @@ var subscriptionInvoiceTools = [
|
|
|
23086
23112
|
];
|
|
23087
23113
|
|
|
23088
23114
|
// src/tools/subscription-items.ts
|
|
23115
|
+
var LIST_SUBSCRIPTION_ITEMS_FILTERS = ["subscriptionId", "priceId"];
|
|
23089
23116
|
var subscriptionItemTools = [
|
|
23090
23117
|
{
|
|
23091
23118
|
name: "ls_get_subscription_item",
|
|
@@ -23107,7 +23134,7 @@ var subscriptionItemTools = [
|
|
|
23107
23134
|
{
|
|
23108
23135
|
name: "ls_list_subscription_items",
|
|
23109
23136
|
authorityClass: "read",
|
|
23110
|
-
description:
|
|
23137
|
+
description: `List all subscription items, optionally filtered by subscription or price. Results are paginated \u2014 check meta.page in the response for currentPage, lastPage, and total. ${crossStoreFilterNote(LIST_SUBSCRIPTION_ITEMS_FILTERS)}`,
|
|
23111
23138
|
annotations: {
|
|
23112
23139
|
title: "List subscription items",
|
|
23113
23140
|
readOnlyHint: true,
|
|
@@ -23122,7 +23149,7 @@ var subscriptionItemTools = [
|
|
|
23122
23149
|
pageNumber: external_exports.number().int().min(1).optional().describe("Page number (1-indexed)"),
|
|
23123
23150
|
pageSize: external_exports.number().int().min(1).max(100).optional().describe("Results per page (1-100)")
|
|
23124
23151
|
}),
|
|
23125
|
-
requiredFilters:
|
|
23152
|
+
requiredFilters: LIST_SUBSCRIPTION_ITEMS_FILTERS,
|
|
23126
23153
|
handler: listHandler("/subscription-items", { subscriptionId: "subscription_id", priceId: "price_id" })
|
|
23127
23154
|
},
|
|
23128
23155
|
{
|
|
@@ -23229,7 +23256,7 @@ var subscriptionTools = [
|
|
|
23229
23256
|
{
|
|
23230
23257
|
name: "ls_update_subscription",
|
|
23231
23258
|
authorityClass: "recurring",
|
|
23232
|
-
description: "Update a subscription. Can change the variant (plan switch), pause/unpause, set billing anchor, or update invoice details. Use ls_cancel_subscription for cancellation.",
|
|
23259
|
+
description: "Update a subscription. Can change the variant (plan switch), pause/unpause, set billing anchor, or update invoice details. Pausing (`pause: 'void'` or `'free'`) or switching plan (`variantId`) is customer-impacting and is treated as destructive (rate-limited and audited); resuming and the billing-neutral edits are not. Use ls_cancel_subscription for cancellation.",
|
|
23233
23260
|
annotations: {
|
|
23234
23261
|
title: "Update subscription",
|
|
23235
23262
|
readOnlyHint: false,
|
|
@@ -23269,6 +23296,11 @@ var subscriptionTools = [
|
|
|
23269
23296
|
if (input.invoiceImmediately !== void 0) attributes.invoice_immediately = input.invoiceImmediately;
|
|
23270
23297
|
if (input.disableProrations !== void 0) attributes.disable_prorations = input.disableProrations;
|
|
23271
23298
|
if (input.trialEndsAt !== void 0) attributes.trial_ends_at = input.trialEndsAt;
|
|
23299
|
+
if (Object.keys(attributes).length === 0) {
|
|
23300
|
+
throw new ToolInputError(
|
|
23301
|
+
"ls_update_subscription requires at least one of: variantId, pause, cancelled, billingAnchor, invoiceImmediately, disableProrations, trialEndsAt"
|
|
23302
|
+
);
|
|
23303
|
+
}
|
|
23272
23304
|
return apiPatch(`/subscriptions/${encodePath(input.subscriptionId)}`, {
|
|
23273
23305
|
data: {
|
|
23274
23306
|
type: "subscriptions",
|
|
@@ -23299,6 +23331,7 @@ var subscriptionTools = [
|
|
|
23299
23331
|
];
|
|
23300
23332
|
|
|
23301
23333
|
// src/tools/usage-records.ts
|
|
23334
|
+
var LIST_USAGE_RECORDS_FILTERS = ["subscriptionItemId"];
|
|
23302
23335
|
var usageRecordTools = [
|
|
23303
23336
|
{
|
|
23304
23337
|
name: "ls_get_usage_record",
|
|
@@ -23320,7 +23353,7 @@ var usageRecordTools = [
|
|
|
23320
23353
|
{
|
|
23321
23354
|
name: "ls_list_usage_records",
|
|
23322
23355
|
authorityClass: "read",
|
|
23323
|
-
description:
|
|
23356
|
+
description: `List all usage records, optionally filtered by subscription item. Results are paginated \u2014 check meta.page in the response for currentPage, lastPage, and total. ${crossStoreFilterNote(LIST_USAGE_RECORDS_FILTERS)}`,
|
|
23324
23357
|
annotations: {
|
|
23325
23358
|
title: "List usage records",
|
|
23326
23359
|
readOnlyHint: true,
|
|
@@ -23334,7 +23367,7 @@ var usageRecordTools = [
|
|
|
23334
23367
|
pageNumber: external_exports.number().int().min(1).optional().describe("Page number (1-indexed)"),
|
|
23335
23368
|
pageSize: external_exports.number().int().min(1).max(100).optional().describe("Results per page (1-100)")
|
|
23336
23369
|
}),
|
|
23337
|
-
requiredFilters:
|
|
23370
|
+
requiredFilters: LIST_USAGE_RECORDS_FILTERS,
|
|
23338
23371
|
handler: listHandler("/usage-records", { subscriptionItemId: "subscription_item_id" })
|
|
23339
23372
|
},
|
|
23340
23373
|
{
|
|
@@ -23397,6 +23430,7 @@ var userTools = [
|
|
|
23397
23430
|
];
|
|
23398
23431
|
|
|
23399
23432
|
// src/tools/variants.ts
|
|
23433
|
+
var LIST_VARIANTS_FILTERS = ["productId"];
|
|
23400
23434
|
var variantTools = [
|
|
23401
23435
|
{
|
|
23402
23436
|
name: "ls_get_variant",
|
|
@@ -23418,7 +23452,7 @@ var variantTools = [
|
|
|
23418
23452
|
{
|
|
23419
23453
|
name: "ls_list_variants",
|
|
23420
23454
|
authorityClass: "read",
|
|
23421
|
-
description:
|
|
23455
|
+
description: `List all variants, optionally filtered by product. Results are paginated \u2014 check meta.page in the response for currentPage, lastPage, and total. ${crossStoreFilterNote(LIST_VARIANTS_FILTERS)}`,
|
|
23422
23456
|
annotations: {
|
|
23423
23457
|
title: "List variants",
|
|
23424
23458
|
readOnlyHint: true,
|
|
@@ -23432,7 +23466,7 @@ var variantTools = [
|
|
|
23432
23466
|
pageNumber: external_exports.number().int().min(1).optional().describe("Page number (1-indexed)"),
|
|
23433
23467
|
pageSize: external_exports.number().int().min(1).max(100).optional().describe("Results per page (1-100)")
|
|
23434
23468
|
}),
|
|
23435
|
-
requiredFilters:
|
|
23469
|
+
requiredFilters: LIST_VARIANTS_FILTERS,
|
|
23436
23470
|
handler: listHandler("/variants", { productId: "product_id" })
|
|
23437
23471
|
}
|
|
23438
23472
|
];
|
|
@@ -23539,7 +23573,7 @@ var webhookTools = [
|
|
|
23539
23573
|
if (input.events !== void 0) attributes.events = input.events;
|
|
23540
23574
|
if (input.secret !== void 0) attributes.secret = input.secret;
|
|
23541
23575
|
if (Object.keys(attributes).length === 0) {
|
|
23542
|
-
throw new
|
|
23576
|
+
throw new ToolInputError("ls_update_webhook requires at least one of: url, events, secret");
|
|
23543
23577
|
}
|
|
23544
23578
|
return apiPatch(`/webhooks/${encodePath(input.webhookId)}`, {
|
|
23545
23579
|
data: {
|
|
@@ -23610,30 +23644,32 @@ function looksLikeBearerToken(value) {
|
|
|
23610
23644
|
if (value.length < 20) return false;
|
|
23611
23645
|
return JWT_VALUE_RE.test(value);
|
|
23612
23646
|
}
|
|
23613
|
-
function redactInner(value,
|
|
23647
|
+
function redactInner(value, ancestors, memo, depth) {
|
|
23614
23648
|
if (depth > MAX_DEPTH) return CIRCULAR;
|
|
23615
23649
|
if (typeof value === "string" && looksLikeBearerToken(value)) return REDACTED;
|
|
23616
23650
|
if (value === null || typeof value !== "object") return value;
|
|
23617
|
-
|
|
23618
|
-
|
|
23619
|
-
|
|
23620
|
-
|
|
23621
|
-
|
|
23622
|
-
|
|
23623
|
-
|
|
23624
|
-
|
|
23625
|
-
|
|
23626
|
-
|
|
23627
|
-
|
|
23628
|
-
|
|
23629
|
-
|
|
23630
|
-
out[key] = redactInner(val, visited, depth + 1);
|
|
23651
|
+
const isArray = Array.isArray(value);
|
|
23652
|
+
if (!isArray && !isPlainObject3(value)) return value;
|
|
23653
|
+
if (ancestors.has(value)) return CIRCULAR;
|
|
23654
|
+
const cached3 = memo.get(value);
|
|
23655
|
+
if (cached3 !== void 0 && cached3.depth <= depth) return cached3.result;
|
|
23656
|
+
ancestors.add(value);
|
|
23657
|
+
let result;
|
|
23658
|
+
if (isArray) {
|
|
23659
|
+
result = value.map((item) => redactInner(item, ancestors, memo, depth + 1));
|
|
23660
|
+
} else {
|
|
23661
|
+
const out = {};
|
|
23662
|
+
for (const [key, val] of Object.entries(value)) {
|
|
23663
|
+
out[key] = SECRET_KEY_RE.test(key) ? REDACTED : redactInner(val, ancestors, memo, depth + 1);
|
|
23631
23664
|
}
|
|
23665
|
+
result = out;
|
|
23632
23666
|
}
|
|
23633
|
-
|
|
23667
|
+
ancestors.delete(value);
|
|
23668
|
+
memo.set(value, { depth, result });
|
|
23669
|
+
return result;
|
|
23634
23670
|
}
|
|
23635
23671
|
function redactSecrets(input) {
|
|
23636
|
-
return redactInner(input, /* @__PURE__ */ new WeakSet(), 0);
|
|
23672
|
+
return redactInner(input, /* @__PURE__ */ new WeakSet(), /* @__PURE__ */ new WeakMap(), 0);
|
|
23637
23673
|
}
|
|
23638
23674
|
|
|
23639
23675
|
// src/wrapper.ts
|
|
@@ -23655,10 +23691,23 @@ function createToolHandler(tool) {
|
|
|
23655
23691
|
}
|
|
23656
23692
|
const inputAsRecord = input;
|
|
23657
23693
|
const toolAsRecord = tool;
|
|
23658
|
-
|
|
23694
|
+
let isDestructive;
|
|
23695
|
+
try {
|
|
23696
|
+
isDestructive = isDestructiveCall(toolAsRecord, inputAsRecord);
|
|
23697
|
+
} catch (predicateErr) {
|
|
23698
|
+
isDestructive = true;
|
|
23699
|
+
logEvent({
|
|
23700
|
+
event: "tool_call",
|
|
23701
|
+
tool: tool.name,
|
|
23702
|
+
status: "exception",
|
|
23703
|
+
latency_ms: 0,
|
|
23704
|
+
error: `isDestructive predicate for tool "${tool.name}" threw: ${predicateErr instanceof Error ? predicateErr.message : String(predicateErr)}. Treating the call as destructive.`
|
|
23705
|
+
});
|
|
23706
|
+
}
|
|
23659
23707
|
const start = Date.now();
|
|
23660
23708
|
try {
|
|
23661
23709
|
checkClassAllowed(tool.authorityClass);
|
|
23710
|
+
tool.preflight?.(input);
|
|
23662
23711
|
checkClassRateLimit(tool.authorityClass);
|
|
23663
23712
|
if (isDestructive) checkDestructiveRateLimit();
|
|
23664
23713
|
checkStoreScopedToolInput(toolAsRecord, inputAsRecord);
|
|
@@ -23704,7 +23753,10 @@ function createToolHandler(tool) {
|
|
|
23704
23753
|
const errorEntry = {
|
|
23705
23754
|
event: "tool_call",
|
|
23706
23755
|
tool: tool.name,
|
|
23707
|
-
|
|
23756
|
+
// Three distinct buckets, because they mean three different things
|
|
23757
|
+
// to whoever is reading the log: operator policy refused the call,
|
|
23758
|
+
// the client sent a bad request, or something actually broke.
|
|
23759
|
+
status: err instanceof GuardrailError ? "guardrail_block" : err instanceof ToolInputError ? "validation_error" : "exception",
|
|
23708
23760
|
latency_ms,
|
|
23709
23761
|
error: message,
|
|
23710
23762
|
audit: isDestructive ? true : void 0,
|
|
@@ -23737,7 +23789,7 @@ function readAuditLogResource(uri) {
|
|
|
23737
23789
|
}
|
|
23738
23790
|
|
|
23739
23791
|
// src/index.ts
|
|
23740
|
-
var version2 = true ? "0.
|
|
23792
|
+
var version2 = true ? "0.11.0" : createRequire(import.meta.url)("../package.json").version;
|
|
23741
23793
|
var subcommand = process.argv[2];
|
|
23742
23794
|
if (subcommand === "version" || subcommand === "--version") {
|
|
23743
23795
|
console.log(version2);
|
|
@@ -23790,5 +23842,9 @@ server.resource(
|
|
|
23790
23842
|
async (uri) => readAuditLogResource(uri)
|
|
23791
23843
|
);
|
|
23792
23844
|
var transport = new StdioServerTransport();
|
|
23793
|
-
|
|
23845
|
+
server.connect(transport).catch((err) => {
|
|
23846
|
+
process.stderr.write(`lemonsqueezy-mcp: ${err instanceof Error ? err.message : String(err)}
|
|
23847
|
+
`);
|
|
23848
|
+
process.exit(1);
|
|
23849
|
+
});
|
|
23794
23850
|
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yawlabs/lemonsqueezy-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
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",
|
|
@@ -32,27 +32,29 @@
|
|
|
32
32
|
"build": "tsc && node build.mjs",
|
|
33
33
|
"dev": "tsc --watch",
|
|
34
34
|
"start": "node dist/index.js",
|
|
35
|
-
"test": "npm run build && node --test dist/**/*.test.js",
|
|
35
|
+
"test": "npm run build && node --test \"dist/**/*.test.js\"",
|
|
36
36
|
"test:integration": "npm run build && node --test dist/integration/*.test.js",
|
|
37
37
|
"lint": "biome check src/",
|
|
38
38
|
"lint:fix": "biome check --write src/",
|
|
39
|
+
"check:oam": "oam check",
|
|
40
|
+
"build:binary:oam": "node scripts/build-binary.mjs --oam",
|
|
39
41
|
"gen:containerfile": "node scripts/sync-containerfile.mjs",
|
|
40
42
|
"check:containerfile": "node scripts/sync-containerfile.mjs --check",
|
|
41
43
|
"prepublishOnly": "npm run build"
|
|
42
44
|
},
|
|
43
|
-
"dependencies": {},
|
|
44
45
|
"overrides": {
|
|
45
46
|
"hono": "^4.12.14"
|
|
46
47
|
},
|
|
47
48
|
"devDependencies": {
|
|
48
49
|
"@biomejs/biome": "^2.4.12",
|
|
49
50
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
50
|
-
"@types/node": "^
|
|
51
|
-
"esbuild": "^0.28.
|
|
52
|
-
"
|
|
51
|
+
"@types/node": "^26.1.1",
|
|
52
|
+
"esbuild": "^0.28.1",
|
|
53
|
+
"postject": "^1.0.0-alpha.6",
|
|
54
|
+
"typescript": "^7.0.2",
|
|
53
55
|
"zod": "^4.3.6"
|
|
54
56
|
},
|
|
55
57
|
"engines": {
|
|
56
|
-
"node": ">=
|
|
58
|
+
"node": ">=22"
|
|
57
59
|
}
|
|
58
60
|
}
|