@yawlabs/lemonsqueezy-mcp 0.10.13 → 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 +195 -148
- package/package.json +3 -1
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
|
@@ -6889,6 +6889,9 @@ var require_dist = __commonJS({
|
|
|
6889
6889
|
}
|
|
6890
6890
|
});
|
|
6891
6891
|
|
|
6892
|
+
// src/index.ts
|
|
6893
|
+
import "node:module";
|
|
6894
|
+
|
|
6892
6895
|
// node_modules/zod/v3/external.js
|
|
6893
6896
|
var external_exports = {};
|
|
6894
6897
|
__export(external_exports, {
|
|
@@ -21108,6 +21111,12 @@ var GuardrailError = class extends Error {
|
|
|
21108
21111
|
this.name = "GuardrailError";
|
|
21109
21112
|
}
|
|
21110
21113
|
};
|
|
21114
|
+
var ToolInputError = class extends Error {
|
|
21115
|
+
constructor(message) {
|
|
21116
|
+
super(message);
|
|
21117
|
+
this.name = "ToolInputError";
|
|
21118
|
+
}
|
|
21119
|
+
};
|
|
21111
21120
|
var AUTHORITY_CLASSES = ["read", "pii", "mutate", "money", "recurring", "key", "webhook"];
|
|
21112
21121
|
function isAuthorityClass(s) {
|
|
21113
21122
|
return AUTHORITY_CLASSES.includes(s);
|
|
@@ -21292,7 +21301,14 @@ function getLogLevel() {
|
|
|
21292
21301
|
if (v === "error") return "error";
|
|
21293
21302
|
return "off";
|
|
21294
21303
|
}
|
|
21295
|
-
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
|
+
]);
|
|
21296
21312
|
function isErrorEntry(entry) {
|
|
21297
21313
|
if (entry.error !== void 0) return true;
|
|
21298
21314
|
if (typeof entry.status === "string") return ERROR_STATUS_TAGS.has(entry.status);
|
|
@@ -21374,6 +21390,7 @@ var JITTER_FRACTION = 0.25;
|
|
|
21374
21390
|
function parseRetryAfterMs(header) {
|
|
21375
21391
|
if (!header) return DEFAULT_RETRY_WAIT_MS;
|
|
21376
21392
|
const trimmed = header.trim();
|
|
21393
|
+
if (trimmed === "") return DEFAULT_RETRY_WAIT_MS;
|
|
21377
21394
|
const seconds = Number(trimmed);
|
|
21378
21395
|
if (!Number.isNaN(seconds)) {
|
|
21379
21396
|
return seconds >= 0 ? seconds * 1e3 : DEFAULT_RETRY_WAIT_MS;
|
|
@@ -21586,7 +21603,10 @@ function buildQuery(params) {
|
|
|
21586
21603
|
if (!params) return "";
|
|
21587
21604
|
const parts = [];
|
|
21588
21605
|
if (params.include?.length) {
|
|
21589
|
-
|
|
21606
|
+
const segments = params.include.map((s) => s.trim()).filter(Boolean);
|
|
21607
|
+
if (segments.length > 0) {
|
|
21608
|
+
parts.push(`include=${encodeURIComponent(segments.join(","))}`);
|
|
21609
|
+
}
|
|
21590
21610
|
}
|
|
21591
21611
|
if (params.filter) {
|
|
21592
21612
|
for (const [key, value] of Object.entries(params.filter)) {
|
|
@@ -21606,6 +21626,49 @@ function buildQuery(params) {
|
|
|
21606
21626
|
function decorateError(error2, requestId) {
|
|
21607
21627
|
return requestId ? `${error2} (request_id: ${requestId})` : error2;
|
|
21608
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
|
+
}
|
|
21609
21672
|
function formatTimeoutMessage(err, fallbackElapsedMs) {
|
|
21610
21673
|
const plural = (n) => `${n} attempt${n === 1 ? "" : "s"}`;
|
|
21611
21674
|
if (isRetryTimeoutError(err)) {
|
|
@@ -21649,43 +21712,7 @@ async function apiRequest(method, path, body) {
|
|
|
21649
21712
|
if (res.status === 401 || res.status === 403) {
|
|
21650
21713
|
invalidateApiKeyCache();
|
|
21651
21714
|
}
|
|
21652
|
-
|
|
21653
|
-
try {
|
|
21654
|
-
const parsed = JSON.parse(errorBody);
|
|
21655
|
-
const detail = parsed.errors?.[0]?.detail ?? errorBody;
|
|
21656
|
-
logEvent({
|
|
21657
|
-
event: "http_call",
|
|
21658
|
-
method,
|
|
21659
|
-
path,
|
|
21660
|
-
status: res.status,
|
|
21661
|
-
latency_ms,
|
|
21662
|
-
request_id: requestId,
|
|
21663
|
-
error: detail
|
|
21664
|
-
});
|
|
21665
|
-
return {
|
|
21666
|
-
ok: false,
|
|
21667
|
-
status: res.status,
|
|
21668
|
-
data: parsed,
|
|
21669
|
-
error: decorateError(detail, requestId),
|
|
21670
|
-
requestId
|
|
21671
|
-
};
|
|
21672
|
-
} catch {
|
|
21673
|
-
logEvent({
|
|
21674
|
-
event: "http_call",
|
|
21675
|
-
method,
|
|
21676
|
-
path,
|
|
21677
|
-
status: res.status,
|
|
21678
|
-
latency_ms,
|
|
21679
|
-
request_id: requestId,
|
|
21680
|
-
error: errorBody
|
|
21681
|
-
});
|
|
21682
|
-
return {
|
|
21683
|
-
ok: false,
|
|
21684
|
-
status: res.status,
|
|
21685
|
-
error: decorateError(errorBody, requestId),
|
|
21686
|
-
requestId
|
|
21687
|
-
};
|
|
21688
|
-
}
|
|
21715
|
+
return handleErrorResponse(res, { method, path }, latency_ms, requestId);
|
|
21689
21716
|
}
|
|
21690
21717
|
logEvent({
|
|
21691
21718
|
event: "http_call",
|
|
@@ -21698,9 +21725,7 @@ async function apiRequest(method, path, body) {
|
|
|
21698
21725
|
if (res.status === 204) {
|
|
21699
21726
|
return { ok: true, status: res.status, requestId };
|
|
21700
21727
|
}
|
|
21701
|
-
|
|
21702
|
-
const data = bodyText.trim() ? JSON.parse(bodyText) : void 0;
|
|
21703
|
-
return { ok: true, status: res.status, data, requestId };
|
|
21728
|
+
return { ok: true, status: res.status, data: await readJsonBody(res), requestId };
|
|
21704
21729
|
}
|
|
21705
21730
|
async function licenseRequest(path, body) {
|
|
21706
21731
|
const url = `${BASE_URL}${path}`;
|
|
@@ -21740,44 +21765,7 @@ async function licenseRequest(path, body) {
|
|
|
21740
21765
|
const latency_ms = Date.now() - start;
|
|
21741
21766
|
const requestId = res.headers.get("x-request-id") ?? void 0;
|
|
21742
21767
|
if (!res.ok) {
|
|
21743
|
-
|
|
21744
|
-
try {
|
|
21745
|
-
const parsed = JSON.parse(errorBody);
|
|
21746
|
-
const p = parsed;
|
|
21747
|
-
const detail = p.errors?.[0]?.detail ?? p.error ?? errorBody;
|
|
21748
|
-
logEvent({
|
|
21749
|
-
event: "http_call",
|
|
21750
|
-
method: "POST",
|
|
21751
|
-
path,
|
|
21752
|
-
status: res.status,
|
|
21753
|
-
latency_ms,
|
|
21754
|
-
request_id: requestId,
|
|
21755
|
-
error: detail
|
|
21756
|
-
});
|
|
21757
|
-
return {
|
|
21758
|
-
ok: false,
|
|
21759
|
-
status: res.status,
|
|
21760
|
-
data: parsed,
|
|
21761
|
-
error: decorateError(detail, requestId),
|
|
21762
|
-
requestId
|
|
21763
|
-
};
|
|
21764
|
-
} catch {
|
|
21765
|
-
logEvent({
|
|
21766
|
-
event: "http_call",
|
|
21767
|
-
method: "POST",
|
|
21768
|
-
path,
|
|
21769
|
-
status: res.status,
|
|
21770
|
-
latency_ms,
|
|
21771
|
-
request_id: requestId,
|
|
21772
|
-
error: errorBody
|
|
21773
|
-
});
|
|
21774
|
-
return {
|
|
21775
|
-
ok: false,
|
|
21776
|
-
status: res.status,
|
|
21777
|
-
error: decorateError(errorBody, requestId),
|
|
21778
|
-
requestId
|
|
21779
|
-
};
|
|
21780
|
-
}
|
|
21768
|
+
return handleErrorResponse(res, { method: "POST", path }, latency_ms, requestId);
|
|
21781
21769
|
}
|
|
21782
21770
|
logEvent({
|
|
21783
21771
|
event: "http_call",
|
|
@@ -21787,9 +21775,7 @@ async function licenseRequest(path, body) {
|
|
|
21787
21775
|
latency_ms,
|
|
21788
21776
|
request_id: requestId
|
|
21789
21777
|
});
|
|
21790
|
-
|
|
21791
|
-
const data = bodyText.trim() ? JSON.parse(bodyText) : void 0;
|
|
21792
|
-
return { ok: true, status: res.status, data, requestId };
|
|
21778
|
+
return { ok: true, status: res.status, data: await readJsonBody(res), requestId };
|
|
21793
21779
|
}
|
|
21794
21780
|
function getHandler(endpoint, idField) {
|
|
21795
21781
|
return async (input) => {
|
|
@@ -21813,6 +21799,32 @@ function listHandler(endpoint, filterMap = {}) {
|
|
|
21813
21799
|
};
|
|
21814
21800
|
return Object.assign(handler, { filterMap });
|
|
21815
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
|
+
}
|
|
21816
21828
|
async function apiGet(path) {
|
|
21817
21829
|
return apiRequest("GET", path);
|
|
21818
21830
|
}
|
|
@@ -21848,7 +21860,9 @@ var affiliateTools = [
|
|
|
21848
21860
|
{
|
|
21849
21861
|
name: "ls_list_affiliates",
|
|
21850
21862
|
authorityClass: "read",
|
|
21851
|
-
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
|
+
)}`,
|
|
21852
21866
|
annotations: {
|
|
21853
21867
|
title: "List affiliates",
|
|
21854
21868
|
readOnlyHint: true,
|
|
@@ -22044,7 +22058,7 @@ var customerTools = [
|
|
|
22044
22058
|
{
|
|
22045
22059
|
name: "ls_update_customer",
|
|
22046
22060
|
authorityClass: "pii",
|
|
22047
|
-
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.",
|
|
22048
22062
|
annotations: {
|
|
22049
22063
|
title: "Update customer",
|
|
22050
22064
|
readOnlyHint: false,
|
|
@@ -22078,6 +22092,11 @@ var customerTools = [
|
|
|
22078
22092
|
if (input.region !== void 0) attributes.region = input.region;
|
|
22079
22093
|
if (input.country !== void 0) attributes.country = input.country;
|
|
22080
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
|
+
}
|
|
22081
22100
|
return apiPatch(`/customers/${encodePath(input.customerId)}`, {
|
|
22082
22101
|
data: {
|
|
22083
22102
|
type: "customers",
|
|
@@ -22114,6 +22133,7 @@ var customerTools = [
|
|
|
22114
22133
|
];
|
|
22115
22134
|
|
|
22116
22135
|
// src/tools/discount-redemptions.ts
|
|
22136
|
+
var LIST_DISCOUNT_REDEMPTIONS_FILTERS = ["discountId", "orderId"];
|
|
22117
22137
|
var discountRedemptionTools = [
|
|
22118
22138
|
{
|
|
22119
22139
|
name: "ls_get_discount_redemption",
|
|
@@ -22135,7 +22155,7 @@ var discountRedemptionTools = [
|
|
|
22135
22155
|
{
|
|
22136
22156
|
name: "ls_list_discount_redemptions",
|
|
22137
22157
|
authorityClass: "read",
|
|
22138
|
-
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)}`,
|
|
22139
22159
|
annotations: {
|
|
22140
22160
|
title: "List discount redemptions",
|
|
22141
22161
|
readOnlyHint: true,
|
|
@@ -22150,7 +22170,7 @@ var discountRedemptionTools = [
|
|
|
22150
22170
|
pageNumber: external_exports.number().int().min(1).optional().describe("Page number (1-indexed)"),
|
|
22151
22171
|
pageSize: external_exports.number().int().min(1).max(100).optional().describe("Results per page (1-100)")
|
|
22152
22172
|
}),
|
|
22153
|
-
requiredFilters:
|
|
22173
|
+
requiredFilters: LIST_DISCOUNT_REDEMPTIONS_FILTERS,
|
|
22154
22174
|
handler: listHandler("/discount-redemptions", { discountId: "discount_id", orderId: "order_id" })
|
|
22155
22175
|
}
|
|
22156
22176
|
];
|
|
@@ -22273,6 +22293,7 @@ var discountTools = [
|
|
|
22273
22293
|
];
|
|
22274
22294
|
|
|
22275
22295
|
// src/tools/files.ts
|
|
22296
|
+
var LIST_FILES_FILTERS = ["variantId"];
|
|
22276
22297
|
var fileTools = [
|
|
22277
22298
|
{
|
|
22278
22299
|
name: "ls_get_file",
|
|
@@ -22294,7 +22315,7 @@ var fileTools = [
|
|
|
22294
22315
|
{
|
|
22295
22316
|
name: "ls_list_files",
|
|
22296
22317
|
authorityClass: "read",
|
|
22297
|
-
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)}`,
|
|
22298
22319
|
annotations: {
|
|
22299
22320
|
title: "List files",
|
|
22300
22321
|
readOnlyHint: true,
|
|
@@ -22308,12 +22329,13 @@ var fileTools = [
|
|
|
22308
22329
|
pageNumber: external_exports.number().int().min(1).optional().describe("Page number (1-indexed)"),
|
|
22309
22330
|
pageSize: external_exports.number().int().min(1).max(100).optional().describe("Results per page (1-100)")
|
|
22310
22331
|
}),
|
|
22311
|
-
requiredFilters:
|
|
22332
|
+
requiredFilters: LIST_FILES_FILTERS,
|
|
22312
22333
|
handler: listHandler("/files", { variantId: "variant_id" })
|
|
22313
22334
|
}
|
|
22314
22335
|
];
|
|
22315
22336
|
|
|
22316
22337
|
// src/tools/license-key-instances.ts
|
|
22338
|
+
var LIST_LICENSE_KEY_INSTANCES_FILTERS = ["licenseKeyId"];
|
|
22317
22339
|
var licenseKeyInstanceTools = [
|
|
22318
22340
|
{
|
|
22319
22341
|
name: "ls_get_license_key_instance",
|
|
@@ -22335,7 +22357,7 @@ var licenseKeyInstanceTools = [
|
|
|
22335
22357
|
{
|
|
22336
22358
|
name: "ls_list_license_key_instances",
|
|
22337
22359
|
authorityClass: "read",
|
|
22338
|
-
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)}`,
|
|
22339
22361
|
annotations: {
|
|
22340
22362
|
title: "List license key instances",
|
|
22341
22363
|
readOnlyHint: true,
|
|
@@ -22349,7 +22371,7 @@ var licenseKeyInstanceTools = [
|
|
|
22349
22371
|
pageNumber: external_exports.number().int().min(1).optional().describe("Page number (1-indexed)"),
|
|
22350
22372
|
pageSize: external_exports.number().int().min(1).max(100).optional().describe("Results per page (1-100)")
|
|
22351
22373
|
}),
|
|
22352
|
-
requiredFilters:
|
|
22374
|
+
requiredFilters: LIST_LICENSE_KEY_INSTANCES_FILTERS,
|
|
22353
22375
|
handler: listHandler("/license-key-instances", { licenseKeyId: "license_key_id" })
|
|
22354
22376
|
}
|
|
22355
22377
|
];
|
|
@@ -22434,6 +22456,11 @@ var licenseKeyTools = [
|
|
|
22434
22456
|
if (input.activationLimit !== void 0) attributes.activation_limit = input.activationLimit;
|
|
22435
22457
|
if (input.disabled !== void 0) attributes.disabled = input.disabled;
|
|
22436
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
|
+
}
|
|
22437
22464
|
return apiPatch(`/license-keys/${encodePath(input.licenseKeyId)}`, {
|
|
22438
22465
|
data: {
|
|
22439
22466
|
type: "license-keys",
|
|
@@ -22515,6 +22542,7 @@ var licenseTools = [
|
|
|
22515
22542
|
];
|
|
22516
22543
|
|
|
22517
22544
|
// src/tools/order-items.ts
|
|
22545
|
+
var LIST_ORDER_ITEMS_FILTERS = ["orderId", "productId", "variantId"];
|
|
22518
22546
|
var orderItemTools = [
|
|
22519
22547
|
{
|
|
22520
22548
|
name: "ls_get_order_item",
|
|
@@ -22536,7 +22564,7 @@ var orderItemTools = [
|
|
|
22536
22564
|
{
|
|
22537
22565
|
name: "ls_list_order_items",
|
|
22538
22566
|
authorityClass: "read",
|
|
22539
|
-
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)}`,
|
|
22540
22568
|
annotations: {
|
|
22541
22569
|
title: "List order items",
|
|
22542
22570
|
readOnlyHint: true,
|
|
@@ -22552,7 +22580,7 @@ var orderItemTools = [
|
|
|
22552
22580
|
pageNumber: external_exports.number().int().min(1).optional().describe("Page number (1-indexed)"),
|
|
22553
22581
|
pageSize: external_exports.number().int().min(1).max(100).optional().describe("Results per page (1-100)")
|
|
22554
22582
|
}),
|
|
22555
|
-
requiredFilters:
|
|
22583
|
+
requiredFilters: LIST_ORDER_ITEMS_FILTERS,
|
|
22556
22584
|
handler: listHandler("/order-items", { orderId: "order_id", productId: "product_id", variantId: "variant_id" })
|
|
22557
22585
|
}
|
|
22558
22586
|
];
|
|
@@ -22623,17 +22651,7 @@ var orderTools = [
|
|
|
22623
22651
|
locale: external_exports.string().max(1e4).optional().describe("Invoice language locale (e.g. 'en', 'fr', 'de')")
|
|
22624
22652
|
}),
|
|
22625
22653
|
handler: async (input) => {
|
|
22626
|
-
|
|
22627
|
-
if (input.name !== void 0) params.set("name", input.name);
|
|
22628
|
-
if (input.address !== void 0) params.set("address", input.address);
|
|
22629
|
-
if (input.city !== void 0) params.set("city", input.city);
|
|
22630
|
-
if (input.state !== void 0) params.set("state", input.state);
|
|
22631
|
-
if (input.zipCode !== void 0) params.set("zip_code", input.zipCode);
|
|
22632
|
-
if (input.country !== void 0) params.set("country", input.country);
|
|
22633
|
-
if (input.notes !== void 0) params.set("notes", input.notes);
|
|
22634
|
-
if (input.locale !== void 0) params.set("locale", input.locale);
|
|
22635
|
-
const qs = params.toString();
|
|
22636
|
-
return apiPost(`/orders/${encodePath(input.orderId)}/generate-invoice${qs ? `?${qs}` : ""}`);
|
|
22654
|
+
return apiPost(`/orders/${encodePath(input.orderId)}/generate-invoice${buildInvoiceQuery(input)}`);
|
|
22637
22655
|
}
|
|
22638
22656
|
},
|
|
22639
22657
|
{
|
|
@@ -22651,6 +22669,12 @@ var orderTools = [
|
|
|
22651
22669
|
orderId: lsIdSchema.describe("The order ID to refund"),
|
|
22652
22670
|
amount: external_exports.number().int().min(1).describe("Refund amount in cents (e.g. 1000 = $10.00)")
|
|
22653
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),
|
|
22654
22678
|
handler: async (input) => {
|
|
22655
22679
|
checkRefundAmount(input.amount);
|
|
22656
22680
|
return apiPost(`/orders/${encodePath(input.orderId)}/refund`, {
|
|
@@ -22661,6 +22685,7 @@ var orderTools = [
|
|
|
22661
22685
|
];
|
|
22662
22686
|
|
|
22663
22687
|
// src/tools/prices.ts
|
|
22688
|
+
var LIST_PRICES_FILTERS = ["variantId"];
|
|
22664
22689
|
var priceTools = [
|
|
22665
22690
|
{
|
|
22666
22691
|
name: "ls_get_price",
|
|
@@ -22682,7 +22707,7 @@ var priceTools = [
|
|
|
22682
22707
|
{
|
|
22683
22708
|
name: "ls_list_prices",
|
|
22684
22709
|
authorityClass: "read",
|
|
22685
|
-
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)}`,
|
|
22686
22711
|
annotations: {
|
|
22687
22712
|
title: "List prices",
|
|
22688
22713
|
readOnlyHint: true,
|
|
@@ -22696,7 +22721,7 @@ var priceTools = [
|
|
|
22696
22721
|
pageNumber: external_exports.number().int().min(1).optional().describe("Page number (1-indexed)"),
|
|
22697
22722
|
pageSize: external_exports.number().int().min(1).max(100).optional().describe("Results per page (1-100)")
|
|
22698
22723
|
}),
|
|
22699
|
-
requiredFilters:
|
|
22724
|
+
requiredFilters: LIST_PRICES_FILTERS,
|
|
22700
22725
|
handler: listHandler("/prices", { variantId: "variant_id" })
|
|
22701
22726
|
}
|
|
22702
22727
|
];
|
|
@@ -22852,10 +22877,11 @@ async function sinkRequest(config2, method, pathAndQuery) {
|
|
|
22852
22877
|
}
|
|
22853
22878
|
const text = readResult.text;
|
|
22854
22879
|
if (!text.trim()) return { ok: true, data: {} };
|
|
22855
|
-
|
|
22880
|
+
const byteLength = Buffer.byteLength(text, "utf8");
|
|
22881
|
+
if (byteLength > MAX_BODY_SIZE_BYTES) {
|
|
22856
22882
|
return {
|
|
22857
22883
|
ok: false,
|
|
22858
|
-
error: `Sink response body too large: ${
|
|
22884
|
+
error: `Sink response body too large: ${byteLength} bytes exceeds ${MAX_BODY_SIZE_BYTES} byte limit`
|
|
22859
22885
|
};
|
|
22860
22886
|
}
|
|
22861
22887
|
try {
|
|
@@ -22959,7 +22985,9 @@ var storeTools = [
|
|
|
22959
22985
|
{
|
|
22960
22986
|
name: "ls_list_stores",
|
|
22961
22987
|
authorityClass: "read",
|
|
22962
|
-
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
|
+
)}`,
|
|
22963
22991
|
annotations: {
|
|
22964
22992
|
title: "List stores",
|
|
22965
22993
|
readOnlyHint: true,
|
|
@@ -23047,18 +23075,8 @@ var subscriptionInvoiceTools = [
|
|
|
23047
23075
|
locale: external_exports.string().max(1e4).optional().describe("Invoice language locale (e.g. 'en', 'fr', 'de')")
|
|
23048
23076
|
}),
|
|
23049
23077
|
handler: async (input) => {
|
|
23050
|
-
const params = new URLSearchParams();
|
|
23051
|
-
if (input.name !== void 0) params.set("name", input.name);
|
|
23052
|
-
if (input.address !== void 0) params.set("address", input.address);
|
|
23053
|
-
if (input.city !== void 0) params.set("city", input.city);
|
|
23054
|
-
if (input.state !== void 0) params.set("state", input.state);
|
|
23055
|
-
if (input.zipCode !== void 0) params.set("zip_code", input.zipCode);
|
|
23056
|
-
if (input.country !== void 0) params.set("country", input.country);
|
|
23057
|
-
if (input.notes !== void 0) params.set("notes", input.notes);
|
|
23058
|
-
if (input.locale !== void 0) params.set("locale", input.locale);
|
|
23059
|
-
const qs = params.toString();
|
|
23060
23078
|
return apiPost(
|
|
23061
|
-
`/subscription-invoices/${encodePath(input.subscriptionInvoiceId)}/generate-invoice${
|
|
23079
|
+
`/subscription-invoices/${encodePath(input.subscriptionInvoiceId)}/generate-invoice${buildInvoiceQuery(input)}`
|
|
23062
23080
|
);
|
|
23063
23081
|
}
|
|
23064
23082
|
},
|
|
@@ -23077,6 +23095,9 @@ var subscriptionInvoiceTools = [
|
|
|
23077
23095
|
subscriptionInvoiceId: lsIdSchema.describe("The subscription invoice ID to refund"),
|
|
23078
23096
|
amount: external_exports.number().int().min(1).describe("Refund amount in cents (e.g. 1000 = $10.00)")
|
|
23079
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),
|
|
23080
23101
|
handler: async (input) => {
|
|
23081
23102
|
checkRefundAmount(input.amount);
|
|
23082
23103
|
return apiPost(`/subscription-invoices/${encodePath(input.subscriptionInvoiceId)}/refund`, {
|
|
@@ -23091,6 +23112,7 @@ var subscriptionInvoiceTools = [
|
|
|
23091
23112
|
];
|
|
23092
23113
|
|
|
23093
23114
|
// src/tools/subscription-items.ts
|
|
23115
|
+
var LIST_SUBSCRIPTION_ITEMS_FILTERS = ["subscriptionId", "priceId"];
|
|
23094
23116
|
var subscriptionItemTools = [
|
|
23095
23117
|
{
|
|
23096
23118
|
name: "ls_get_subscription_item",
|
|
@@ -23112,7 +23134,7 @@ var subscriptionItemTools = [
|
|
|
23112
23134
|
{
|
|
23113
23135
|
name: "ls_list_subscription_items",
|
|
23114
23136
|
authorityClass: "read",
|
|
23115
|
-
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)}`,
|
|
23116
23138
|
annotations: {
|
|
23117
23139
|
title: "List subscription items",
|
|
23118
23140
|
readOnlyHint: true,
|
|
@@ -23127,7 +23149,7 @@ var subscriptionItemTools = [
|
|
|
23127
23149
|
pageNumber: external_exports.number().int().min(1).optional().describe("Page number (1-indexed)"),
|
|
23128
23150
|
pageSize: external_exports.number().int().min(1).max(100).optional().describe("Results per page (1-100)")
|
|
23129
23151
|
}),
|
|
23130
|
-
requiredFilters:
|
|
23152
|
+
requiredFilters: LIST_SUBSCRIPTION_ITEMS_FILTERS,
|
|
23131
23153
|
handler: listHandler("/subscription-items", { subscriptionId: "subscription_id", priceId: "price_id" })
|
|
23132
23154
|
},
|
|
23133
23155
|
{
|
|
@@ -23234,7 +23256,7 @@ var subscriptionTools = [
|
|
|
23234
23256
|
{
|
|
23235
23257
|
name: "ls_update_subscription",
|
|
23236
23258
|
authorityClass: "recurring",
|
|
23237
|
-
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.",
|
|
23238
23260
|
annotations: {
|
|
23239
23261
|
title: "Update subscription",
|
|
23240
23262
|
readOnlyHint: false,
|
|
@@ -23274,6 +23296,11 @@ var subscriptionTools = [
|
|
|
23274
23296
|
if (input.invoiceImmediately !== void 0) attributes.invoice_immediately = input.invoiceImmediately;
|
|
23275
23297
|
if (input.disableProrations !== void 0) attributes.disable_prorations = input.disableProrations;
|
|
23276
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
|
+
}
|
|
23277
23304
|
return apiPatch(`/subscriptions/${encodePath(input.subscriptionId)}`, {
|
|
23278
23305
|
data: {
|
|
23279
23306
|
type: "subscriptions",
|
|
@@ -23304,6 +23331,7 @@ var subscriptionTools = [
|
|
|
23304
23331
|
];
|
|
23305
23332
|
|
|
23306
23333
|
// src/tools/usage-records.ts
|
|
23334
|
+
var LIST_USAGE_RECORDS_FILTERS = ["subscriptionItemId"];
|
|
23307
23335
|
var usageRecordTools = [
|
|
23308
23336
|
{
|
|
23309
23337
|
name: "ls_get_usage_record",
|
|
@@ -23325,7 +23353,7 @@ var usageRecordTools = [
|
|
|
23325
23353
|
{
|
|
23326
23354
|
name: "ls_list_usage_records",
|
|
23327
23355
|
authorityClass: "read",
|
|
23328
|
-
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)}`,
|
|
23329
23357
|
annotations: {
|
|
23330
23358
|
title: "List usage records",
|
|
23331
23359
|
readOnlyHint: true,
|
|
@@ -23339,7 +23367,7 @@ var usageRecordTools = [
|
|
|
23339
23367
|
pageNumber: external_exports.number().int().min(1).optional().describe("Page number (1-indexed)"),
|
|
23340
23368
|
pageSize: external_exports.number().int().min(1).max(100).optional().describe("Results per page (1-100)")
|
|
23341
23369
|
}),
|
|
23342
|
-
requiredFilters:
|
|
23370
|
+
requiredFilters: LIST_USAGE_RECORDS_FILTERS,
|
|
23343
23371
|
handler: listHandler("/usage-records", { subscriptionItemId: "subscription_item_id" })
|
|
23344
23372
|
},
|
|
23345
23373
|
{
|
|
@@ -23402,6 +23430,7 @@ var userTools = [
|
|
|
23402
23430
|
];
|
|
23403
23431
|
|
|
23404
23432
|
// src/tools/variants.ts
|
|
23433
|
+
var LIST_VARIANTS_FILTERS = ["productId"];
|
|
23405
23434
|
var variantTools = [
|
|
23406
23435
|
{
|
|
23407
23436
|
name: "ls_get_variant",
|
|
@@ -23423,7 +23452,7 @@ var variantTools = [
|
|
|
23423
23452
|
{
|
|
23424
23453
|
name: "ls_list_variants",
|
|
23425
23454
|
authorityClass: "read",
|
|
23426
|
-
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)}`,
|
|
23427
23456
|
annotations: {
|
|
23428
23457
|
title: "List variants",
|
|
23429
23458
|
readOnlyHint: true,
|
|
@@ -23437,7 +23466,7 @@ var variantTools = [
|
|
|
23437
23466
|
pageNumber: external_exports.number().int().min(1).optional().describe("Page number (1-indexed)"),
|
|
23438
23467
|
pageSize: external_exports.number().int().min(1).max(100).optional().describe("Results per page (1-100)")
|
|
23439
23468
|
}),
|
|
23440
|
-
requiredFilters:
|
|
23469
|
+
requiredFilters: LIST_VARIANTS_FILTERS,
|
|
23441
23470
|
handler: listHandler("/variants", { productId: "product_id" })
|
|
23442
23471
|
}
|
|
23443
23472
|
];
|
|
@@ -23544,7 +23573,7 @@ var webhookTools = [
|
|
|
23544
23573
|
if (input.events !== void 0) attributes.events = input.events;
|
|
23545
23574
|
if (input.secret !== void 0) attributes.secret = input.secret;
|
|
23546
23575
|
if (Object.keys(attributes).length === 0) {
|
|
23547
|
-
throw new
|
|
23576
|
+
throw new ToolInputError("ls_update_webhook requires at least one of: url, events, secret");
|
|
23548
23577
|
}
|
|
23549
23578
|
return apiPatch(`/webhooks/${encodePath(input.webhookId)}`, {
|
|
23550
23579
|
data: {
|
|
@@ -23615,30 +23644,32 @@ function looksLikeBearerToken(value) {
|
|
|
23615
23644
|
if (value.length < 20) return false;
|
|
23616
23645
|
return JWT_VALUE_RE.test(value);
|
|
23617
23646
|
}
|
|
23618
|
-
function redactInner(value,
|
|
23647
|
+
function redactInner(value, ancestors, memo, depth) {
|
|
23619
23648
|
if (depth > MAX_DEPTH) return CIRCULAR;
|
|
23620
23649
|
if (typeof value === "string" && looksLikeBearerToken(value)) return REDACTED;
|
|
23621
23650
|
if (value === null || typeof value !== "object") return value;
|
|
23622
|
-
|
|
23623
|
-
|
|
23624
|
-
|
|
23625
|
-
|
|
23626
|
-
|
|
23627
|
-
|
|
23628
|
-
|
|
23629
|
-
|
|
23630
|
-
|
|
23631
|
-
|
|
23632
|
-
|
|
23633
|
-
|
|
23634
|
-
|
|
23635
|
-
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);
|
|
23636
23664
|
}
|
|
23665
|
+
result = out;
|
|
23637
23666
|
}
|
|
23638
|
-
|
|
23667
|
+
ancestors.delete(value);
|
|
23668
|
+
memo.set(value, { depth, result });
|
|
23669
|
+
return result;
|
|
23639
23670
|
}
|
|
23640
23671
|
function redactSecrets(input) {
|
|
23641
|
-
return redactInner(input, /* @__PURE__ */ new WeakSet(), 0);
|
|
23672
|
+
return redactInner(input, /* @__PURE__ */ new WeakSet(), /* @__PURE__ */ new WeakMap(), 0);
|
|
23642
23673
|
}
|
|
23643
23674
|
|
|
23644
23675
|
// src/wrapper.ts
|
|
@@ -23660,10 +23691,23 @@ function createToolHandler(tool) {
|
|
|
23660
23691
|
}
|
|
23661
23692
|
const inputAsRecord = input;
|
|
23662
23693
|
const toolAsRecord = tool;
|
|
23663
|
-
|
|
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
|
+
}
|
|
23664
23707
|
const start = Date.now();
|
|
23665
23708
|
try {
|
|
23666
23709
|
checkClassAllowed(tool.authorityClass);
|
|
23710
|
+
tool.preflight?.(input);
|
|
23667
23711
|
checkClassRateLimit(tool.authorityClass);
|
|
23668
23712
|
if (isDestructive) checkDestructiveRateLimit();
|
|
23669
23713
|
checkStoreScopedToolInput(toolAsRecord, inputAsRecord);
|
|
@@ -23709,7 +23753,10 @@ function createToolHandler(tool) {
|
|
|
23709
23753
|
const errorEntry = {
|
|
23710
23754
|
event: "tool_call",
|
|
23711
23755
|
tool: tool.name,
|
|
23712
|
-
|
|
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",
|
|
23713
23760
|
latency_ms,
|
|
23714
23761
|
error: message,
|
|
23715
23762
|
audit: isDestructive ? true : void 0,
|
|
@@ -23742,7 +23789,7 @@ function readAuditLogResource(uri) {
|
|
|
23742
23789
|
}
|
|
23743
23790
|
|
|
23744
23791
|
// src/index.ts
|
|
23745
|
-
var version2 = true ? "0.
|
|
23792
|
+
var version2 = true ? "0.11.0" : createRequire(import.meta.url)("../package.json").version;
|
|
23746
23793
|
var subcommand = process.argv[2];
|
|
23747
23794
|
if (subcommand === "version" || subcommand === "--version") {
|
|
23748
23795
|
console.log(version2);
|
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",
|
|
@@ -36,6 +36,8 @@
|
|
|
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"
|