@yawlabs/lemonsqueezy-mcp 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +19 -2
  2. package/dist/index.js +102 -35
  3. package/package.json +3 -4
package/README.md CHANGED
@@ -174,9 +174,9 @@ All configuration is via environment variables. Only `LEMONSQUEEZY_API_KEY` (or
174
174
  | --- | --- |
175
175
  | `LEMONSQUEEZY_API_KEY` | LemonSqueezy API token. |
176
176
  | `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. |
177
- | `LEMONSQUEEZY_ALLOWED_STORE_IDS` | Comma-separated allowlist of store IDs. When set, tools that receive a `storeId` input reject calls to any other store. Note: operations that don't take an explicit `storeId` (e.g. `ls_refund_order`) are not gated by this — pair with `LEMONSQUEEZY_MAX_REFUND_AMOUNT_CENTS`. |
177
+ | `LEMONSQUEEZY_ALLOWED_STORE_IDS` | Comma-separated allowlist of store IDs. When set: (1) any tool whose input includes a `storeId` rejects calls to a non-allowed store; (2) tools that *accept* a `storeId` filter (e.g. `ls_list_orders`, `ls_list_subscriptions`) require it — calls without one are blocked so a missing filter cannot return data from every store the API key can see. Tools with no `storeId` field at all (e.g. `ls_refund_order`, `ls_list_stores`) are not gated by this — pair with `LEMONSQUEEZY_MAX_REFUND_AMOUNT_CENTS` and `LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT`. |
178
178
  | `LEMONSQUEEZY_MAX_REFUND_AMOUNT_CENTS` | Rejects `ls_refund_order` and `ls_refund_subscription_invoice` calls above this amount. |
179
- | `LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT` | Max destructive tool calls per 60-second rolling window. In-process limit — per MCP server instance, not global. |
179
+ | `LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT` | Max destructive tool calls per 60-second rolling window. In-process limit — per MCP server instance, not global; each `npx` cold start resets the window. Counts include `ls_update_license_key` calls that set `disabled: true`. |
180
180
  | `LEMONSQUEEZY_LOG=json` | Emit one JSON log line to stderr per tool and HTTP call. Destructive calls are tagged `audit: true` and include their inputs. |
181
181
 
182
182
  ### Logging format
@@ -214,6 +214,23 @@ npm test # full unit + handler suite
214
214
  npm run test:integration # requires LEMONSQUEEZY_TEST_API_KEY + LEMONSQUEEZY_TEST_STORE_ID
215
215
  ```
216
216
 
217
+ ## Releasing
218
+
219
+ Releases are cut locally — there is no CI pipeline. From a clean checkout of `main`:
220
+
221
+ ```bash
222
+ ./release.sh 0.6.0
223
+ ```
224
+
225
+ The script lints, tests, builds, bumps the version, commits and tags, pushes to `origin`, publishes to npm, and creates a GitHub release. Each step is idempotent — re-running with the same version after a partial failure resumes from where it stopped.
226
+
227
+ One-time setup on the release machine:
228
+
229
+ ```bash
230
+ npm login --auth-type=web # publisher of @yawlabs/lemonsqueezy-mcp
231
+ gh auth login # GitHub CLI for the release-creation step
232
+ ```
233
+
217
234
  ## License
218
235
 
219
236
  MIT
package/dist/index.js CHANGED
@@ -30176,6 +30176,24 @@ function checkDestructiveRateLimit(now = Date.now()) {
30176
30176
  }
30177
30177
  destructiveTimestamps.push(now);
30178
30178
  }
30179
+ function isStoreAllowlistActive() {
30180
+ return loadOptions().allowedStoreIds !== null;
30181
+ }
30182
+ function checkStoreScopedToolInput(toolAcceptsStoreId, input) {
30183
+ if (!toolAcceptsStoreId) return;
30184
+ const raw = input.storeId;
30185
+ if (raw !== void 0 && raw !== null && raw !== "") {
30186
+ checkStoreAllowed(String(raw));
30187
+ return;
30188
+ }
30189
+ if (isStoreAllowlistActive()) {
30190
+ throw new GuardrailError("storeId is required when LEMONSQUEEZY_ALLOWED_STORE_IDS is set");
30191
+ }
30192
+ }
30193
+ function isDestructiveCall(tool, input) {
30194
+ if (typeof tool.isDestructive === "function") return tool.isDestructive(input);
30195
+ return tool.annotations?.destructiveHint === true;
30196
+ }
30179
30197
 
30180
30198
  // src/logger.ts
30181
30199
  function isEnabled() {
@@ -30186,6 +30204,25 @@ function logEvent(entry) {
30186
30204
  try {
30187
30205
  const line = JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), ...entry });
30188
30206
  process.stderr.write(`${line}
30207
+ `);
30208
+ return;
30209
+ } catch {
30210
+ }
30211
+ try {
30212
+ const fallback = JSON.stringify({
30213
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
30214
+ event: entry.event,
30215
+ tool: entry.tool,
30216
+ method: entry.method,
30217
+ path: entry.path,
30218
+ status: entry.status,
30219
+ latency_ms: entry.latency_ms,
30220
+ request_id: entry.request_id,
30221
+ audit: entry.audit,
30222
+ error: entry.error,
30223
+ log_error: "inputs_not_serializable"
30224
+ });
30225
+ process.stderr.write(`${fallback}
30189
30226
  `);
30190
30227
  } catch {
30191
30228
  }
@@ -30326,6 +30363,9 @@ async function loadApiKey() {
30326
30363
 
30327
30364
  // src/api.ts
30328
30365
  var BASE_URL = "https://api.lemonsqueezy.com/v1";
30366
+ function encodePath(segment) {
30367
+ return encodeURIComponent(String(segment));
30368
+ }
30329
30369
  function buildQuery(params) {
30330
30370
  if (!params) return "";
30331
30371
  const parts = [];
@@ -30362,7 +30402,7 @@ async function apiRequest(method, path, body) {
30362
30402
  headers["Content-Type"] = "application/vnd.api+json";
30363
30403
  fetchBody = JSON.stringify(body);
30364
30404
  }
30365
- const url2 = path.startsWith("http") ? path : `${BASE_URL}${path}`;
30405
+ const url2 = `${BASE_URL}${path}`;
30366
30406
  const idempotent = method === "GET" || method === "DELETE";
30367
30407
  let res;
30368
30408
  try {
@@ -30430,7 +30470,8 @@ async function apiRequest(method, path, body) {
30430
30470
  if (res.status === 204) {
30431
30471
  return { ok: true, status: res.status, requestId };
30432
30472
  }
30433
- const data = await res.json();
30473
+ const bodyText = await res.text();
30474
+ const data = bodyText.trim() ? JSON.parse(bodyText) : void 0;
30434
30475
  return { ok: true, status: res.status, data, requestId };
30435
30476
  }
30436
30477
  async function licenseRequest(path, body) {
@@ -30518,13 +30559,14 @@ async function licenseRequest(path, body) {
30518
30559
  latency_ms,
30519
30560
  request_id: requestId
30520
30561
  });
30521
- const data = await res.json();
30562
+ const bodyText = await res.text();
30563
+ const data = bodyText.trim() ? JSON.parse(bodyText) : void 0;
30522
30564
  return { ok: true, status: res.status, data, requestId };
30523
30565
  }
30524
30566
  function getHandler(endpoint, idField) {
30525
30567
  return async (input) => {
30526
30568
  const query = buildQuery({ include: input.include?.split(",") });
30527
- return apiGet(`${endpoint}/${input[idField]}${query}`);
30569
+ return apiGet(`${endpoint}/${encodePath(input[idField])}${query}`);
30528
30570
  };
30529
30571
  }
30530
30572
  function listHandler(endpoint, filterMap = {}) {
@@ -30662,13 +30704,11 @@ var checkoutTools = [
30662
30704
  const checkoutData = {};
30663
30705
  if (input.email !== void 0) checkoutData.email = input.email;
30664
30706
  if (input.name !== void 0) checkoutData.name = input.name;
30665
- if (input.billingAddressCountry !== void 0)
30666
- checkoutData.billing_address = { country: input.billingAddressCountry };
30667
- if (input.billingAddressZip !== void 0) {
30668
- checkoutData.billing_address = {
30669
- ...checkoutData.billing_address || {},
30670
- zip: input.billingAddressZip
30671
- };
30707
+ if (input.billingAddressCountry !== void 0 || input.billingAddressZip !== void 0) {
30708
+ const billingAddress = {};
30709
+ if (input.billingAddressCountry !== void 0) billingAddress.country = input.billingAddressCountry;
30710
+ if (input.billingAddressZip !== void 0) billingAddress.zip = input.billingAddressZip;
30711
+ checkoutData.billing_address = billingAddress;
30672
30712
  }
30673
30713
  if (input.taxNumber !== void 0) checkoutData.tax_number = input.taxNumber;
30674
30714
  if (input.discountCode !== void 0) checkoutData.discount_code = input.discountCode;
@@ -30774,6 +30814,12 @@ var customerTools = [
30774
30814
  idempotentHint: true,
30775
30815
  openWorldHint: true
30776
30816
  },
30817
+ // Setting status to "archived" via this tool is the same operation as
30818
+ // ls_archive_customer and must engage the same rate limiter / audit log;
30819
+ // otherwise the dedicated archive tool's destructive flag becomes a side
30820
+ // channel anyone can route around. Other field edits (name, email,
30821
+ // address) stay on the regular path.
30822
+ isDestructive: (input) => input.status === "archived",
30777
30823
  inputSchema: external_exports3.object({
30778
30824
  customerId: external_exports3.string().max(1e4).describe("The customer ID to update"),
30779
30825
  name: external_exports3.string().max(1e4).optional().describe("New name"),
@@ -30791,7 +30837,7 @@ var customerTools = [
30791
30837
  if (input.region !== void 0) attributes.region = input.region;
30792
30838
  if (input.country !== void 0) attributes.country = input.country;
30793
30839
  if (input.status !== void 0) attributes.status = input.status;
30794
- return apiPatch(`/customers/${input.customerId}`, {
30840
+ return apiPatch(`/customers/${encodePath(input.customerId)}`, {
30795
30841
  data: {
30796
30842
  type: "customers",
30797
30843
  id: input.customerId,
@@ -30806,7 +30852,7 @@ var customerTools = [
30806
30852
  annotations: {
30807
30853
  title: "Archive customer",
30808
30854
  readOnlyHint: false,
30809
- destructiveHint: false,
30855
+ destructiveHint: true,
30810
30856
  idempotentHint: true,
30811
30857
  openWorldHint: true
30812
30858
  },
@@ -30814,7 +30860,7 @@ var customerTools = [
30814
30860
  customerId: external_exports3.string().max(1e4).describe("The customer ID to archive")
30815
30861
  }),
30816
30862
  handler: async (input) => {
30817
- return apiPatch(`/customers/${input.customerId}`, {
30863
+ return apiPatch(`/customers/${encodePath(input.customerId)}`, {
30818
30864
  data: {
30819
30865
  type: "customers",
30820
30866
  id: input.customerId,
@@ -30972,7 +31018,7 @@ var discountTools = [
30972
31018
  discountId: external_exports3.string().max(1e4).describe("The discount ID to delete")
30973
31019
  }),
30974
31020
  handler: async (input) => {
30975
- return apiDelete(`/discounts/${input.discountId}`);
31021
+ return apiDelete(`/discounts/${encodePath(input.discountId)}`);
30976
31022
  }
30977
31023
  }
30978
31024
  ];
@@ -31103,7 +31149,7 @@ var licenseKeyTools = [
31103
31149
  },
31104
31150
  {
31105
31151
  name: "ls_update_license_key",
31106
- description: "Update a license key's activation limit, expiry date, or disabled status.",
31152
+ description: "Update a license key's activation limit, expiry date, or disabled status. Setting `disabled: true` revokes customer access and is treated as destructive (rate-limited and audited).",
31107
31153
  annotations: {
31108
31154
  title: "Update license key",
31109
31155
  readOnlyHint: false,
@@ -31111,18 +31157,23 @@ var licenseKeyTools = [
31111
31157
  idempotentHint: true,
31112
31158
  openWorldHint: true
31113
31159
  },
31160
+ // Disabling a license key revokes a customer's access. Tag the call as
31161
+ // destructive only when `disabled: true` so the rate limiter and audit log
31162
+ // engage on revocation, while benign edits (expiry, activation limit) stay
31163
+ // on the regular path.
31164
+ isDestructive: (input) => input.disabled === true,
31114
31165
  inputSchema: external_exports3.object({
31115
31166
  licenseKeyId: external_exports3.string().max(1e4).describe("The license key ID to update"),
31116
31167
  activationLimit: external_exports3.number().int().min(0).optional().describe("Maximum number of activations allowed (0 = unlimited)"),
31117
31168
  disabled: external_exports3.boolean().optional().describe("Set to true to disable this license key"),
31118
- expiresAt: external_exports3.string().max(1e4).optional().describe("Expiry date (ISO 8601 format). Set to null to remove expiry.")
31169
+ expiresAt: external_exports3.string().max(1e4).nullable().optional().describe("Expiry date (ISO 8601 format). Set to null to remove expiry.")
31119
31170
  }),
31120
31171
  handler: async (input) => {
31121
31172
  const attributes = {};
31122
31173
  if (input.activationLimit !== void 0) attributes.activation_limit = input.activationLimit;
31123
31174
  if (input.disabled !== void 0) attributes.disabled = input.disabled;
31124
31175
  if (input.expiresAt !== void 0) attributes.expires_at = input.expiresAt;
31125
- return apiPatch(`/license-keys/${input.licenseKeyId}`, {
31176
+ return apiPatch(`/license-keys/${encodePath(input.licenseKeyId)}`, {
31126
31177
  data: {
31127
31178
  type: "license-keys",
31128
31179
  id: input.licenseKeyId,
@@ -31312,7 +31363,7 @@ var orderTools = [
31312
31363
  if (input.notes !== void 0) params.set("notes", input.notes);
31313
31364
  if (input.locale !== void 0) params.set("locale", input.locale);
31314
31365
  const qs = params.toString();
31315
- return apiPost(`/orders/${input.orderId}/generate-invoice${qs ? `?${qs}` : ""}`);
31366
+ return apiPost(`/orders/${encodePath(input.orderId)}/generate-invoice${qs ? `?${qs}` : ""}`);
31316
31367
  }
31317
31368
  },
31318
31369
  {
@@ -31331,7 +31382,7 @@ var orderTools = [
31331
31382
  }),
31332
31383
  handler: async (input) => {
31333
31384
  checkRefundAmount(input.amount);
31334
- return apiPost(`/orders/${input.orderId}/refund`, {
31385
+ return apiPost(`/orders/${encodePath(input.orderId)}/refund`, {
31335
31386
  data: { type: "orders", id: input.orderId, attributes: { amount: input.amount } }
31336
31387
  });
31337
31388
  }
@@ -31531,7 +31582,9 @@ var subscriptionInvoiceTools = [
31531
31582
  if (input.notes !== void 0) params.set("notes", input.notes);
31532
31583
  if (input.locale !== void 0) params.set("locale", input.locale);
31533
31584
  const qs = params.toString();
31534
- return apiPost(`/subscription-invoices/${input.subscriptionInvoiceId}/generate-invoice${qs ? `?${qs}` : ""}`);
31585
+ return apiPost(
31586
+ `/subscription-invoices/${encodePath(input.subscriptionInvoiceId)}/generate-invoice${qs ? `?${qs}` : ""}`
31587
+ );
31535
31588
  }
31536
31589
  },
31537
31590
  {
@@ -31550,7 +31603,7 @@ var subscriptionInvoiceTools = [
31550
31603
  }),
31551
31604
  handler: async (input) => {
31552
31605
  checkRefundAmount(input.amount);
31553
- return apiPost(`/subscription-invoices/${input.subscriptionInvoiceId}/refund`, {
31606
+ return apiPost(`/subscription-invoices/${encodePath(input.subscriptionInvoiceId)}/refund`, {
31554
31607
  data: {
31555
31608
  type: "subscription-invoices",
31556
31609
  id: input.subscriptionInvoiceId,
@@ -31604,7 +31657,7 @@ var subscriptionItemTools = [
31604
31657
  annotations: {
31605
31658
  title: "Update subscription item",
31606
31659
  readOnlyHint: false,
31607
- destructiveHint: false,
31660
+ destructiveHint: true,
31608
31661
  idempotentHint: true,
31609
31662
  openWorldHint: true
31610
31663
  },
@@ -31613,7 +31666,7 @@ var subscriptionItemTools = [
31613
31666
  quantity: external_exports3.number().int().min(1).describe("New quantity for the subscription item")
31614
31667
  }),
31615
31668
  handler: async (input) => {
31616
- return apiPatch(`/subscription-items/${input.subscriptionItemId}`, {
31669
+ return apiPatch(`/subscription-items/${encodePath(input.subscriptionItemId)}`, {
31617
31670
  data: {
31618
31671
  type: "subscription-items",
31619
31672
  id: input.subscriptionItemId,
@@ -31636,7 +31689,7 @@ var subscriptionItemTools = [
31636
31689
  subscriptionItemId: external_exports3.string().max(1e4).describe("The subscription item ID")
31637
31690
  }),
31638
31691
  handler: async (input) => {
31639
- return apiGet(`/subscription-items/${input.subscriptionItemId}/current-usage`);
31692
+ return apiGet(`/subscription-items/${encodePath(input.subscriptionItemId)}/current-usage`);
31640
31693
  }
31641
31694
  }
31642
31695
  ];
@@ -31705,9 +31758,20 @@ var subscriptionTools = [
31705
31758
  idempotentHint: true,
31706
31759
  openWorldHint: true
31707
31760
  },
31761
+ // A pause (void/free) or plan switch is the customer-impacting subset of
31762
+ // this tool's surface; treat those calls as destructive so they engage
31763
+ // the rate limiter and audit log. Intentional exclusions, all of which
31764
+ // either reverse a destructive action or are billing-neutral:
31765
+ // - pause === "resume" un-pause, restoring access
31766
+ // - cancelled === false un-cancel before expiry, restoring access
31767
+ // - billingAnchor changes anchor day, no immediate charge
31768
+ // - trialEndsAt extending or ending a trial
31769
+ // - invoiceImmediately toggle on the next prorated edit
31770
+ // - disableProrations toggle on plan-change behavior
31771
+ isDestructive: (input) => typeof input.pause === "string" && input.pause !== "resume" || typeof input.variantId === "string",
31708
31772
  inputSchema: external_exports3.object({
31709
31773
  subscriptionId: external_exports3.string().max(1e4).describe("The subscription ID to update"),
31710
- variantId: external_exports3.string().max(1e4).optional().describe("New variant ID for plan switching"),
31774
+ variantId: external_exports3.string().max(1e4).regex(/^[1-9]\d*$/, "variantId must be a positive integer string (e.g. '12345')").optional().describe("New variant ID for plan switching"),
31711
31775
  pause: external_exports3.enum(["void", "free", "resume"]).optional().describe("Pause mode: 'void' (pause, skip billing), 'free' (pause, keep access free), or 'resume' to unpause"),
31712
31776
  cancelled: external_exports3.literal(false).optional().describe(
31713
31777
  "Set to false to un-cancel a subscription before it expires. To cancel, use ls_cancel_subscription instead."
@@ -31726,7 +31790,7 @@ var subscriptionTools = [
31726
31790
  if (input.invoiceImmediately !== void 0) attributes.invoice_immediately = input.invoiceImmediately;
31727
31791
  if (input.disableProrations !== void 0) attributes.disable_prorations = input.disableProrations;
31728
31792
  if (input.trialEndsAt !== void 0) attributes.trial_ends_at = input.trialEndsAt;
31729
- return apiPatch(`/subscriptions/${input.subscriptionId}`, {
31793
+ return apiPatch(`/subscriptions/${encodePath(input.subscriptionId)}`, {
31730
31794
  data: {
31731
31795
  type: "subscriptions",
31732
31796
  id: input.subscriptionId,
@@ -31749,7 +31813,7 @@ var subscriptionTools = [
31749
31813
  subscriptionId: external_exports3.string().max(1e4).describe("The subscription ID to cancel")
31750
31814
  }),
31751
31815
  handler: async (input) => {
31752
- return apiDelete(`/subscriptions/${input.subscriptionId}`);
31816
+ return apiDelete(`/subscriptions/${encodePath(input.subscriptionId)}`);
31753
31817
  }
31754
31818
  }
31755
31819
  ];
@@ -31796,7 +31860,10 @@ var usageRecordTools = [
31796
31860
  annotations: {
31797
31861
  title: "Create usage record",
31798
31862
  readOnlyHint: false,
31799
- destructiveHint: false,
31863
+ // Usage records feed the meter that determines the customer's bill --
31864
+ // 'increment' adds to current usage, 'set' overwrites it. Either mode
31865
+ // moves real money, so engage the rate limiter and audit log.
31866
+ destructiveHint: true,
31800
31867
  idempotentHint: false,
31801
31868
  openWorldHint: true
31802
31869
  },
@@ -31973,7 +32040,7 @@ var webhookTools = [
31973
32040
  if (input.url !== void 0) attributes.url = input.url;
31974
32041
  if (input.events !== void 0) attributes.events = input.events;
31975
32042
  if (input.secret !== void 0) attributes.secret = input.secret;
31976
- return apiPatch(`/webhooks/${input.webhookId}`, {
32043
+ return apiPatch(`/webhooks/${encodePath(input.webhookId)}`, {
31977
32044
  data: {
31978
32045
  type: "webhooks",
31979
32046
  id: input.webhookId,
@@ -31996,13 +32063,13 @@ var webhookTools = [
31996
32063
  webhookId: external_exports3.string().max(1e4).describe("The webhook ID to delete")
31997
32064
  }),
31998
32065
  handler: async (input) => {
31999
- return apiDelete(`/webhooks/${input.webhookId}`);
32066
+ return apiDelete(`/webhooks/${encodePath(input.webhookId)}`);
32000
32067
  }
32001
32068
  }
32002
32069
  ];
32003
32070
 
32004
32071
  // src/index.ts
32005
- var version2 = true ? "0.5.0" : (await null).createRequire(import.meta.url)("../package.json").version;
32072
+ var version2 = true ? "0.6.1" : (await null).createRequire(import.meta.url)("../package.json").version;
32006
32073
  var subcommand = process.argv[2];
32007
32074
  if (subcommand === "version" || subcommand === "--version") {
32008
32075
  console.log(version2);
@@ -32036,18 +32103,18 @@ var server = new McpServer({
32036
32103
  version: version2
32037
32104
  });
32038
32105
  for (const tool of allTools) {
32039
- const isDestructive = tool.annotations?.destructiveHint === true;
32106
+ const toolAcceptsStoreId = "storeId" in tool.inputSchema.shape;
32040
32107
  server.tool(
32041
32108
  tool.name,
32042
32109
  tool.description,
32043
32110
  tool.inputSchema.shape,
32044
32111
  tool.annotations,
32045
32112
  async (input) => {
32113
+ const isDestructive = isDestructiveCall(tool, input);
32046
32114
  const start = Date.now();
32047
32115
  try {
32048
32116
  if (isDestructive) checkDestructiveRateLimit();
32049
- const storeId = input.storeId;
32050
- if (typeof storeId === "string") checkStoreAllowed(storeId);
32117
+ checkStoreScopedToolInput(toolAcceptsStoreId, input);
32051
32118
  const result = await tool.handler(input);
32052
32119
  const response = result;
32053
32120
  const latency_ms = Date.now() - start;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yawlabs/lemonsqueezy-mcp",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "LemonSqueezy MCP server for managing your store from AI assistants",
5
5
  "license": "MIT",
6
6
  "author": "YawLabs <contact@yaw.sh>",
@@ -31,11 +31,10 @@
31
31
  "dev": "tsc --watch",
32
32
  "start": "node dist/index.js",
33
33
  "test": "npm run build && node --test dist/**/*.test.js",
34
- "test:ci": "npm run test",
35
34
  "test:integration": "npm run build && node --test dist/integration/*.test.js",
36
35
  "lint": "biome check src/",
37
36
  "lint:fix": "biome check --write src/",
38
- "prepublishOnly": "npm run build && node --test dist/**/*.test.js"
37
+ "prepublishOnly": "npm run build"
39
38
  },
40
39
  "dependencies": {},
41
40
  "overrides": {
@@ -50,6 +49,6 @@
50
49
  "zod": "^4.3.6"
51
50
  },
52
51
  "engines": {
53
- "node": ">=18"
52
+ "node": ">=20"
54
53
  }
55
54
  }