fresh-squeezy 0.1.6 → 0.1.8
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/README.md +67 -160
- package/dist/cli.js +391 -207
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +337 -210
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +426 -26
- package/dist/index.d.ts +426 -26
- package/dist/index.js +330 -210
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,81 +1,32 @@
|
|
|
1
1
|
# fresh-squeezy
|
|
2
2
|
|
|
3
|
-
Validator-first Lemon Squeezy
|
|
3
|
+
Validator-first Lemon Squeezy doctor. Catches misconfigurations before they ship. CLI + library, Node 20+.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## Why this exists
|
|
10
|
-
|
|
11
|
-
The official [`@lemonsqueezy/lemonsqueezy.js`](https://github.com/lmsqueezy/lemonsqueezy.js) SDK is great at making API calls. But integration bugs almost never live in the API calls — they live in configuration. Wrong store. Wrong mode. Unpublished product. Webhook registered but missing the one event your refund flow depends on. Live API key accidentally loaded in staging.
|
|
12
|
-
|
|
13
|
-
`fresh-squeezy` answers a different question fast:
|
|
14
|
-
|
|
15
|
-
> Is my API key pointed at the right store, in the right mode, with a product that is actually published and a webhook subscribed to the events my app relies on?
|
|
16
|
-
|
|
17
|
-
### The pain it removes
|
|
18
|
-
|
|
19
|
-
- **Postman + dashboard ping-pong.** Today you copy IDs out of the Lemon Squeezy UI, paste them into env files, and hit Postman to verify each one. One CLI call replaces that loop.
|
|
20
|
-
- **SDK lag.** The official JS SDK last shipped v4.0.0 on 2024-11-05. The platform has added three behaviors since (`affiliate_activated`, `payment_processor`, `customer_updated`) that the SDK does not surface. `fresh-squeezy` tracks them in `src/support/manifest.ts` and a weekly drift workflow files an issue the moment the changelog moves again.
|
|
21
|
-
- **The scariest bug: prod-in-staging.** `fresh-squeezy` calls `/v1/users/me` and compares the `meta.test_mode` flag (API changelog 2024-01-05) against the mode you declared. Mismatch = `MODE_MISMATCH` error, doctor exits 1. Neither the SDK nor a hand-rolled wrapper catches this by default.
|
|
22
|
-
- **Repeat work across products.** Every new SaaS inside a company repeats the same billing setup dance. This is one place for the checks so the next product integration is a five-minute job, not a week of yak-shaving.
|
|
23
|
-
|
|
24
|
-
### Why a validator, not another SDK
|
|
25
|
-
|
|
26
|
-
Most teams already use the official SDK or plain `fetch` for API calls. They don't need another wrapper — they need **fewer silent misconfigurations**. `fresh-squeezy` is intentionally thin:
|
|
27
|
-
|
|
28
|
-
- 7 validators (`connection`, `store`, `product`, `webhook`, `discount`, `licenseKey`, `subscriptionPlan`) that return the same `ValidationResult` shape every time
|
|
29
|
-
- `doctor()` composes them into one report
|
|
30
|
-
- A raw `request()` escape hatch so you never hit a wall when the platform adds something we haven't wrapped yet
|
|
31
|
-
- Static, reviewed support manifest — no live scraping in runtime code
|
|
32
|
-
- Stable issue codes so CI can branch on findings
|
|
33
|
-
|
|
34
|
-
If a check feels magical, something is wrong.
|
|
35
|
-
|
|
36
|
-
### Open source by design
|
|
37
|
-
|
|
38
|
-
This project is MIT-licensed and deliberately scoped to stay small. The goal is to make it easy for any team (ours or yours) to drop it into a new product, wire it into CI, and trust the output. Contributions that keep it boring — more coverage, more validators, better error messages — are exactly the shape we want. See [CONTRIBUTING.md](./CONTRIBUTING.md).
|
|
39
|
-
|
|
40
|
-
---
|
|
41
|
-
|
|
42
|
-
## What it checks
|
|
43
|
-
|
|
44
|
-
| Validator | Catches |
|
|
45
|
-
| ------------ | --------------------------------------------------------------------------------------------------------------------------------- |
|
|
46
|
-
| `connection` | Invalid key, unreachable account, no stores, **declared mode ≠ key's actual mode** (`MODE_MISMATCH`) |
|
|
47
|
-
| `store` | Wrong store ID, store owned by a different account |
|
|
48
|
-
| `product` | Unpublished product, product on the wrong store, missing or all-draft variants, missing buy URL |
|
|
49
|
-
| `webhook` | Webhook URL not registered, missing recommended events (order lifecycle, subscription lifecycle, refunds), missing optional newer events |
|
|
50
|
-
| `discount` | Draft discounts, expired or not-yet-active windows, invalid amounts (≤0 or >100%), store ownership mismatch |
|
|
51
|
-
| `licenseKey` | Disabled keys, expired keys, keys at activation limit, store ownership mismatch |
|
|
52
|
-
| `subscriptionPlan` | Non-subscription variants, invalid billing intervals, zero-price plans, inconsistent trial settings, draft variants, store ownership mismatch |
|
|
53
|
-
|
|
54
|
-
Every validator returns the same `ValidationResult` — stable public contract, switchable by `issue.code`.
|
|
55
|
-
|
|
56
|
-
---
|
|
57
|
-
|
|
58
|
-
## Install
|
|
5
|
+
## 30-second start
|
|
59
6
|
|
|
60
7
|
```bash
|
|
61
|
-
npm
|
|
62
|
-
#
|
|
63
|
-
|
|
8
|
+
npm i -D fresh-squeezy
|
|
9
|
+
cp .env.example .env.local # fill in LEMON_SQUEEZY_API_KEY
|
|
10
|
+
npx fresh-squeezy doctor --all-stores
|
|
64
11
|
```
|
|
65
12
|
|
|
66
|
-
|
|
13
|
+
No store ID to copy from the dashboard — the CLI discovers reachable stores itself.
|
|
67
14
|
|
|
68
|
-
|
|
15
|
+
| Exit | Meaning |
|
|
16
|
+
|------|---------|
|
|
17
|
+
| `0` | All validators passed |
|
|
18
|
+
| `1` | One or more validators reported `error`-level issues |
|
|
19
|
+
| `2` | Fatal (missing key, invalid flags, network failure) |
|
|
69
20
|
|
|
70
|
-
|
|
71
|
-
cp .env.example .env.local
|
|
72
|
-
# fill in LEMON_SQUEEZY_API_KEY (test or live)
|
|
73
|
-
npx fresh-squeezy doctor --all-stores
|
|
74
|
-
```
|
|
21
|
+
## What it catches that Postman and the official SDK won't
|
|
75
22
|
|
|
76
|
-
|
|
23
|
+
- **Prod key pointed at staging.** `MODE_MISMATCH` fires when the key's true `meta.test_mode` (API changelog 2024-01-05) disagrees with the declared mode. Doctor exits 1. Neither the SDK nor a hand-rolled wrapper catches this by default.
|
|
24
|
+
- **Silent store-ownership mismatches.** Products, discounts, license keys, and subscription plans whose `store_id` doesn't match the store you scoped the run to. Stable codes: `PRODUCT_WRONG_STORE`, `DISCOUNT_STORE_MISMATCH`, `LICENSE_KEY_STORE_MISMATCH`, `PLAN_STORE_MISMATCH`.
|
|
25
|
+
- **Webhook subscribed to the wrong events.** Diff against a manifest of recommended events (order/subscription lifecycle, refunds) and newer-but-optional events the SDK doesn't ship.
|
|
26
|
+
- **Platform drift.** A weekly GitHub Action hashes the [Lemon Squeezy API changelog](https://docs.lemonsqueezy.com/api/getting-started/changelog) against `src/support/changelog-snapshot.json` and opens an issue when it moves. Tracked additions beyond the official SDK as of 2026-04-24: `customer_updated` (2026-02-25), `payment_processor` on Subscription (2025-06-11), Affiliates + `affiliate_activated` (2025-01-21), `test_mode` on `/v1/users/me` (2024-01-05).
|
|
27
|
+
- **Postman + dashboard ping-pong.** One `doctor` call replaces the loop of copying IDs out of the UI, pasting them into env files, and verifying each one by hand.
|
|
77
28
|
|
|
78
|
-
##
|
|
29
|
+
## CLI
|
|
79
30
|
|
|
80
31
|
```bash
|
|
81
32
|
# TTY: multi-select stores interactively, run doctor on each
|
|
@@ -87,7 +38,7 @@ npx fresh-squeezy doctor --all-stores
|
|
|
87
38
|
# Specific stores
|
|
88
39
|
npx fresh-squeezy doctor --store-ids 12,34,56
|
|
89
40
|
|
|
90
|
-
# Scope
|
|
41
|
+
# Scope to a product + webhook
|
|
91
42
|
npx fresh-squeezy doctor --store-ids 12 \
|
|
92
43
|
--product-id 987 \
|
|
93
44
|
--webhook-url https://app.example.com/api/webhooks/lemon-squeezy
|
|
@@ -101,24 +52,14 @@ npx fresh-squeezy validate webhook \
|
|
|
101
52
|
npx fresh-squeezy doctor --all-stores --json
|
|
102
53
|
```
|
|
103
54
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
| Code | Meaning |
|
|
107
|
-
| ---- | ---------------------------------------------- |
|
|
108
|
-
| `0` | All validators passed |
|
|
109
|
-
| `1` | One or more validators reported `error`-level |
|
|
110
|
-
| `2` | Fatal error (missing key, invalid flags, etc.) |
|
|
111
|
-
|
|
112
|
-
### Store resolution
|
|
113
|
-
|
|
114
|
-
Resolution order used by every store-scoped command:
|
|
55
|
+
Store resolution order, used by every store-scoped command:
|
|
115
56
|
|
|
116
57
|
1. `--store-ids 1,2,3` (comma-separated, explicit)
|
|
117
58
|
2. `--all-stores` (every store reachable with the key)
|
|
118
|
-
3. TTY: inquirer multi-select
|
|
119
|
-
4. No TTY + no flag:
|
|
59
|
+
3. TTY: inquirer multi-select prompt
|
|
60
|
+
4. No TTY + no flag: connection-only run (useful as a CI smoke check)
|
|
120
61
|
|
|
121
|
-
##
|
|
62
|
+
## Library
|
|
122
63
|
|
|
123
64
|
```ts
|
|
124
65
|
import { createFreshSqueezy } from "fresh-squeezy";
|
|
@@ -126,7 +67,7 @@ import { createFreshSqueezy } from "fresh-squeezy";
|
|
|
126
67
|
const lemon = createFreshSqueezy(); // reads LEMON_SQUEEZY_API_KEY, LEMON_SQUEEZY_MODE
|
|
127
68
|
|
|
128
69
|
const report = await lemon.doctor({
|
|
129
|
-
storeId: 12,
|
|
70
|
+
storeId: 12, // library is single-store per call
|
|
130
71
|
productId: 987,
|
|
131
72
|
webhookUrl: "https://app.example.com/api/webhooks/lemon-squeezy",
|
|
132
73
|
});
|
|
@@ -141,75 +82,41 @@ if (!report.ok) {
|
|
|
141
82
|
}
|
|
142
83
|
```
|
|
143
84
|
|
|
144
|
-
For multi-store runs at the library layer, call `doctor()` in a loop
|
|
85
|
+
For multi-store runs at the library layer, call `doctor()` in a loop. The CLI does exactly this.
|
|
86
|
+
|
|
87
|
+
Public types: [`FreshSqueezyClient`](src/createFreshSqueezy.ts), [`ValidationResult<T>`](src/core/types.ts), [`DoctorReport`](src/core/types.ts). Switch on `issue.code` in CI logic — codes are stable across minor versions.
|
|
145
88
|
|
|
146
89
|
## Sandbox vs live
|
|
147
90
|
|
|
148
|
-
Lemon Squeezy serves both modes from the same API host
|
|
91
|
+
Lemon Squeezy serves both modes from the same API host; mode is determined by the key. `fresh-squeezy` cross-checks the declared mode against `meta.test_mode` from `/v1/users/me`. Mismatch = `MODE_MISMATCH`, doctor exits 1 — the fastest way to catch a prod key pointed at staging before it does damage.
|
|
149
92
|
|
|
150
93
|
```ts
|
|
151
94
|
const lemon = createFreshSqueezy({ mode: "test" });
|
|
152
95
|
const result = await lemon.validateConnection();
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
The CLI default is `--mode test`. Override with `--mode live`.
|
|
158
|
-
|
|
159
|
-
Live smoke testing in CI: the repo ships an opt-in `npm run test:live` target gated on `LEMON_SQUEEZY_LIVE_SMOKE=1`. Run it nightly with a secret test-mode key so platform drift surfaces before a release.
|
|
160
|
-
|
|
161
|
-
## API
|
|
162
|
-
|
|
163
|
-
### `createFreshSqueezy(config?)`
|
|
164
|
-
|
|
165
|
-
```ts
|
|
166
|
-
createFreshSqueezy({
|
|
167
|
-
apiKey?: string; // default: process.env.LEMON_SQUEEZY_API_KEY
|
|
168
|
-
storeId?: string | number; // optional — also read from env for lib consumers
|
|
169
|
-
mode?: "test" | "live"; // default: process.env.LEMON_SQUEEZY_MODE ?? "test"
|
|
170
|
-
baseUrl?: string; // default: "https://api.lemonsqueezy.com"
|
|
171
|
-
fetch?: typeof fetch; // default: globalThis.fetch
|
|
172
|
-
});
|
|
96
|
+
result.mode; // "test" (declared)
|
|
97
|
+
result.resource?.actualMode; // "live" — alarm bell
|
|
173
98
|
```
|
|
174
99
|
|
|
175
|
-
|
|
100
|
+
The CLI default is `--mode test`. Override with `--mode live`. For nightly platform-drift checks in CI, run `npm run test:live` with `LEMON_SQUEEZY_LIVE_SMOKE=1` and a test-mode key.
|
|
176
101
|
|
|
177
|
-
|
|
178
|
-
client.mode
|
|
179
|
-
client.request(options) // raw escape hatch
|
|
180
|
-
client.validateConnection()
|
|
181
|
-
client.validateStore(id)
|
|
182
|
-
client.validateProduct({ productId, expectedStoreId? })
|
|
183
|
-
client.validateWebhook({ storeId, url })
|
|
184
|
-
client.validateDiscount({ storeId, discountId })
|
|
185
|
-
client.validateLicenseKey({ storeId, licenseKeyId })
|
|
186
|
-
client.validateSubscriptionPlan({ storeId, variantId })
|
|
187
|
-
client.doctor({ storeId?, productId?, webhookUrl?, discountId?, licenseKeyId?, variantId? })
|
|
188
|
-
```
|
|
102
|
+
## Issue codes
|
|
189
103
|
|
|
190
|
-
|
|
104
|
+
Switch on `issue.code` in CI. All codes are stable across minor versions.
|
|
191
105
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}>;
|
|
205
|
-
}
|
|
206
|
-
```
|
|
106
|
+
| Code | Meaning |
|
|
107
|
+
|------|---------|
|
|
108
|
+
| `AUTH_FAILED` | Invalid or missing API key |
|
|
109
|
+
| `MODE_MISMATCH` | Declared mode doesn't match key's `meta.test_mode` |
|
|
110
|
+
| `NETWORK_ERROR` | Lemon Squeezy unreachable |
|
|
111
|
+
| `STORE_NOT_FOUND` / `STORE_NOT_OWNED` | Store ID invalid or owned by another account |
|
|
112
|
+
| `PRODUCT_UNPUBLISHED` / `PRODUCT_WRONG_STORE` / `PRODUCT_NO_BUY_URL` | Product can't accept checkout |
|
|
113
|
+
| `VARIANT_MISSING` / `VARIANT_UNPUBLISHED` | Product has no live variants |
|
|
114
|
+
| `WEBHOOK_NOT_FOUND` / `WEBHOOK_EVENTS_MISSING` / `WEBHOOK_OPTIONAL_EVENTS` | Webhook URL not registered or under-subscribed |
|
|
115
|
+
| `DISCOUNT_DRAFT` / `DISCOUNT_EXPIRED` / `DISCOUNT_NOT_STARTED` / `DISCOUNT_INVALID_AMOUNT` / `DISCOUNT_REDEMPTIONS_EXHAUSTED` / `DISCOUNT_STORE_MISMATCH` | Discount won't apply at checkout |
|
|
116
|
+
| `LICENSE_KEY_DISABLED` / `LICENSE_KEY_EXPIRED` / `LICENSE_KEY_AT_ACTIVATION_LIMIT` / `LICENSE_KEY_STORE_MISMATCH` | License key won't activate |
|
|
117
|
+
| `PLAN_NOT_SUBSCRIPTION` / `PLAN_INVALID_INTERVAL` / `PLAN_FREE_PRICE` / `PLAN_TRIAL_INCONSISTENT` / `PLAN_DRAFT` / `PLAN_STORE_MISMATCH` | Subscription plan misconfigured |
|
|
207
118
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
### Raw escape hatch
|
|
211
|
-
|
|
212
|
-
For endpoints not yet wrapped (new changelog entries, License API, affiliates):
|
|
119
|
+
For endpoints not yet wrapped, use the raw escape hatch:
|
|
213
120
|
|
|
214
121
|
```ts
|
|
215
122
|
const user = await lemon.request({ path: "/v1/users/me" });
|
|
@@ -217,36 +124,36 @@ const user = await lemon.request({ path: "/v1/users/me" });
|
|
|
217
124
|
|
|
218
125
|
## Environment variables
|
|
219
126
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
|
223
|
-
|
|
|
224
|
-
| `
|
|
225
|
-
| `LEMON_SQUEEZY_MODE` | no | library + CLI | `test` (default) or `live` |
|
|
226
|
-
| `LEMON_SQUEEZY_STORE_ID` | no | library consumers | Convenience default for `client.doctor()` |
|
|
227
|
-
|
|
228
|
-
The CLI does **not** read `LEMON_SQUEEZY_STORE_ID` — use `--store-ids` or `--all-stores` so store selection stays explicit per-command.
|
|
229
|
-
|
|
230
|
-
## Changelog drift watcher
|
|
127
|
+
| Variable | Required | Purpose |
|
|
128
|
+
|----------|----------|---------|
|
|
129
|
+
| `LEMON_SQUEEZY_API_KEY` | yes | Bearer token (library + CLI) |
|
|
130
|
+
| `LEMON_SQUEEZY_MODE` | no | `test` (default) or `live` |
|
|
131
|
+
| `LEMON_SQUEEZY_STORE_ID` | no | Convenience default for `client.doctor()` — library only |
|
|
231
132
|
|
|
232
|
-
|
|
133
|
+
The CLI does not read `LEMON_SQUEEZY_STORE_ID`; use `--store-ids` or `--all-stores` so store selection stays explicit per command.
|
|
233
134
|
|
|
234
|
-
|
|
135
|
+
## Reference
|
|
235
136
|
|
|
236
|
-
|
|
237
|
-
- `payment_processor` property on Subscription — added **2025-06-11**
|
|
238
|
-
- Affiliates endpoints + `affiliate_activated` webhook — added **2025-01-21**
|
|
239
|
-
- `test_mode` flag on `/v1/users/me` — added **2024-01-05** (powers `MODE_MISMATCH`)
|
|
137
|
+
### Validators
|
|
240
138
|
|
|
241
|
-
|
|
139
|
+
- **`validateConnection`** — Reachability, key validity, store presence, declared-vs-actual mode. [→ source](src/validate/connection.ts)
|
|
140
|
+
- **`validateStore`** — Store ID exists and is owned by the key's account. [→ source](src/validate/store.ts)
|
|
141
|
+
- **`validateProduct`** — Published, on the expected store, has live variants and a buy URL. [→ source](src/validate/product.ts)
|
|
142
|
+
- **`validateWebhook`** — Webhook URL registered and subscribed to recommended events. [→ source](src/validate/webhook.ts)
|
|
143
|
+
- **`validateDiscount`** — Active, in-window, valid amount, store ownership matches. [→ source](src/validate/discount.ts)
|
|
144
|
+
- **`validateLicenseKey`** — Enabled, not expired, activations available, store ownership matches. [→ source](src/validate/licenseKey.ts)
|
|
145
|
+
- **`validateSubscriptionPlan`** — Subscription type, valid interval, non-zero price, consistent trial. [→ source](src/validate/subscriptionPlan.ts)
|
|
146
|
+
- **`doctor`** — Composes the above into one `DoctorReport`. [→ source](src/validate/doctor.ts)
|
|
242
147
|
|
|
243
|
-
|
|
148
|
+
### CLI commands
|
|
244
149
|
|
|
245
|
-
|
|
150
|
+
- **`doctor`** — Run every configured validator and emit a report. [→ source](src/cli/commands/doctor.ts)
|
|
151
|
+
- **`validate <name>`** — Run a single validator. [→ source](src/cli/commands/validate.ts)
|
|
152
|
+
- **`init`** — Interactive setup: ask for credentials, pick a store, run doctor. [→ source](src/cli/commands/init.ts)
|
|
246
153
|
|
|
247
154
|
## Contributing
|
|
248
155
|
|
|
249
|
-
See [CONTRIBUTING.md](./CONTRIBUTING.md).
|
|
156
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md). Clone, `npm install`, `npm test`.
|
|
250
157
|
|
|
251
158
|
## License
|
|
252
159
|
|